Vercel Logo

Type-Safe data layer

TypeScript gives you compile-time type safety, but it can't validate data at runtime. Zod provides both: runtime validation AND TypeScript types inferred from your schemas. This catches bugs early and makes your code reliable.

Outcome

Create a type-safe data layer with Zod schemas for products and reviews, populate it with sample data, and implement helper functions for data access.

Fast Track

  1. Run pnpm add zod and create lib/types.ts with ReviewSchema and ProductSchema
  2. Create lib/sample-data.ts with 3 products, each having 3-4 reviews with varied ratings
  3. Add getProducts() and getProduct(slug) functions, then update app/page.tsx to display product cards

Hands-on Exercise 1.2

Build a type-safe data layer for product reviews:

Requirements:

  1. Install Zod for schema validation
  2. Create schemas for Review and Product types
  3. Add sample data for 3-5 products with multiple reviews each
  4. Create helper functions: getProducts() and getProduct(id)
  5. Export TypeScript types inferred from Zod schemas

Implementation hints:

  • Use z.object() to define schemas
  • Use z.infer<typeof schema> to generate TypeScript types
  • Include fields: product name, slug, description, reviews array
  • Each review needs: reviewer name, stars (1-5), text, date
  • Sample data should feel realistic (varied ratings, detailed reviews)

Step 1: Install Zod

pnpm add zod

Zod is a TypeScript-first schema validation library with zero dependencies.

Step 2: Create Type Schemas

Create lib/types.ts:

lib/types.ts
import { z } from "zod";
 
// Review schema
export const ReviewSchema = z.object({
  reviewer: z.string(),
  stars: z.number().min(1).max(5),
  review: z.string(),
  date: z.string(), // ISO date string
});
 
// Product schema
export const ProductSchema = z.object({
  slug: z.string(),
  name: z.string(),
  description: z.string(),
  reviews: z.array(ReviewSchema),
});
 
// Infer TypeScript types from schemas
export type Review = z.infer<typeof ReviewSchema>;
export type Product = z.infer<typeof ProductSchema>;

What this gives you:

  • Runtime validation with .parse() or .safeParse()
  • Automatic TypeScript types
  • Compile-time AND runtime safety

Step 3: Create Sample Data

Create lib/sample-data.ts:

lib/sample-data.ts
import { Product, ProductSchema } from "./types";
 
export const sampleProductsReviews: Record<string, Product> = {
  mower: {
    slug: "mower",
    name: "Mower3000",
    description: "Autonomous robotic lawn mower with smart navigation",
    reviews: [
      {
        reviewer: "John D.",
        stars: 4,
        review:
          "Great mower! Handles slopes well and is very quiet. Setup took about an hour, but once configured it works autonomously. Battery lasts about 90 minutes.",
        date: "2025-11-15T10:30:00Z",
      },
      {
        reviewer: "Sarah M.",
        stars: 5,
        review:
          "Love this thing! My lawn has never looked better. It runs every day at 6am and I don't have to think about it. The app is easy to use and scheduling is straightforward.",
        date: "2025-11-20T14:22:00Z",
      },
      {
        reviewer: "Mike R.",
        stars: 2,
        review:
          "Disappointed. I hate mowing the lawn, and this did not change that.",
        date: "2025-11-28T08:15:00Z",
      },
      {
        reviewer: "Emily K.",
        stars: 4,
        review:
          "Really impressed with the cutting quality. It mulches the grass perfectly. Only downside is it can't handle thick weeds, but that's expected. Worth the price.",
        date: "2025-12-01T16:45:00Z",
      },
    ],
  },
  ecoBright: {
    slug: "ecoBright",
    name: "EcoBright LED Bulbs",
    description: "Energy-efficient smart LED bulbs with color temperature control",
    reviews: [
      {
        reviewer: "Amanda L.",
        stars: 5,
        review:
          "These bulbs are fantastic! Added a lot of ambiance to the room.",
        date: "2025-11-10T09:20:00Z",
      },
      {
        reviewer: "Carlos P.",
        stars: 3,
        review:
          "Decent bulbs for the price. Color temperature control works well, but I wish they were brighter at max setting. They do save energy compared to my old bulbs.",
        date: "2025-11-18T12:33:00Z",
      },
      {
        reviewer: "Lisa T.",
        stars: 4,
        review:
          "Very happy with these. The scheduling feature is great—bulbs dim automatically at 9pm. App is intuitive. Lost one star because one bulb failed after 3 months.",
        date: "2025-11-25T18:10:00Z",
      },
    ],
  },
  aquaHeat: {
    slug: "aquaHeat",
    name: "AquaHeat Tankless Water Heater",
    description: "High-efficiency tankless water heater with digital temperature control",
    reviews: [
      {
        reviewer: "Robert F.",
        stars: 5,
        review:
          "Incredible upgrade from our old tank heater. Endless hot water and our energy bill dropped by 30%. Installation was professional and took about 4 hours.",
        date: "2025-10-05T11:15:00Z",
      },
      {
        reviewer: "Jenny W.",
        stars: 4,
        review:
          "Works great but required upgrading our gas line which added $800 to the cost. Once installed, it's been flawless. Water heats instantly and temperature is consistent.",
        date: "2025-10-20T15:40:00Z",
      },
      {
        reviewer: "Tom H.",
        stars: 3,
        review:
          "Good product but overpriced. It works as advertised but the 'energy savings' haven't been as dramatic as claimed. Still, no more running out of hot water is nice.",
        date: "2025-11-12T07:55:00Z",
      },
      {
        reviewer: "Maria S.",
        stars: 5,
        review:
          "Best home improvement we've made! Compact design freed up space in our utility room. The digital display is clear and adjusting temperature is easy. Highly recommend.",
        date: "2025-11-30T13:25:00Z",
      },
    ],
  },
};
 
// Validate data at runtime
Object.values(sampleProductsReviews).forEach((product) => {
  ProductSchema.parse(product);
});
 
export const Products = Object.values(sampleProductsReviews);

What this provides:

  • 3 products with varied reviews
  • Realistic review content and ratings
  • Runtime validation (throws if data is malformed)

Step 4: Create Helper Functions

Add data access functions to lib/sample-data.ts:

lib/sample-data.ts
export const Products = Object.values(sampleProductsReviews);
 
/**
 * Add beneath the Products export
 */
export function getProducts(): Product[] {
  return Products;
}
 
/**
 * Get a single product by slug
 * @throws Error if product not found
 */
export function getProduct(slug: string): Product {
  const product = sampleProductsReviews[slug];
 
  if (!product) {
    throw new Error(`Product not found: ${slug}`);
  }
 
  return product;
}

Type safety benefits:

  • getProducts() returns Product[] (fully typed)
  • getProduct() returns Product (throws if not found)
  • TypeScript autocomplete works everywhere

Try It

Test in your app:

Update app/page.tsx to display products:

app/page.tsx
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { getProducts } from "@/lib/sample-data";
 
export default function Home() {
  const products = getProducts();
 
  return (
    <main className="min-h-screen p-8">
      <div className="max-w-4xl mx-auto space-y-8">
        <h1 className="text-4xl font-bold">Product Reviews</h1>
 
        <div className="grid gap-4">
          {products.map((product) => (
            <Card key={product.slug}>
              <CardHeader>
                <CardTitle>{product.name}</CardTitle>
              </CardHeader>
              <CardContent>
                <p className="text-sm text-muted-foreground">
                  {product.description}
                </p>
                <p className="text-sm mt-2">
                  {product.reviews.length} reviews
                </p>
              </CardContent>
            </Card>
          ))}
        </div>
      </div>
    </main>
  );
}

Visit http://localhost:3000

You should see:

  • 3 product cards
  • Product names and descriptions
  • Review counts

Test type safety:

Try accessing a non-existent field:

product.nonExistentField // TypeScript error!

Try passing wrong data:

const badReview = { stars: 10 }; // Will fail Zod validation (max is 5)
ReviewSchema.parse(badReview); // Throws error

Understanding Zod Benefits

Without Zod (plain TypeScript):

type Product = {
  name: string;
  reviews: Review[];
};
 
// Runtime: no validation
// If API returns bad data, your app breaks

With Zod:

const ProductSchema = z.object({
  name: z.string(),
  reviews: z.array(ReviewSchema),
});
 
type Product = z.infer<typeof ProductSchema>;
 
// Runtime: validated with .parse()
// Type errors caught at compile time AND runtime

Key advantages:

  1. Single source of truth - Schema defines both validation and types
  2. Runtime safety - Catches invalid data from APIs, forms, databases
  3. Better errors - Zod errors are detailed and actionable
  4. No type drift - Types automatically match validation rules

Project Structure

Your data layer is now:

lib/
├── types.ts           # Zod schemas + TypeScript types
├── sample-data.ts     # Sample products with reviews
└── utils.ts           # Helper functions (from shadcn)

Done-When

  • Zod installed and schemas created
  • Product and Review types defined
  • Sample data with 3+ products and multiple reviews
  • Helper functions getProducts() and getProduct() implemented
  • Homepage displays product list
  • Full type safety with compile-time and runtime validation

What's Next

Your data layer is solid and type-safe. In the next lesson, you'll build UI components to display individual reviews with star ratings, avatars, and timestamps. These components will use the Product and Review types you just created.


Sources: