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
- Run
pnpm add zodand createlib/types.tswithReviewSchemaandProductSchema - Create
lib/sample-data.tswith 3 products, each having 3-4 reviews with varied ratings - Add
getProducts()andgetProduct(slug)functions, then updateapp/page.tsxto display product cards
Hands-on Exercise 1.2
Build a type-safe data layer for product reviews:
Requirements:
- Install Zod for schema validation
- Create schemas for Review and Product types
- Add sample data for 3-5 products with multiple reviews each
- Create helper functions:
getProducts()andgetProduct(id) - 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 zodZod is a TypeScript-first schema validation library with zero dependencies.
Step 2: Create Type Schemas
Create 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:
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:
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()returnsProduct[](fully typed)getProduct()returnsProduct(throws if not found)- TypeScript autocomplete works everywhere
Try It
Test in your app:
Update app/page.tsx to display products:
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 errorUnderstanding Zod Benefits
Without Zod (plain TypeScript):
type Product = {
name: string;
reviews: Review[];
};
// Runtime: no validation
// If API returns bad data, your app breaksWith 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 runtimeKey advantages:
- Single source of truth - Schema defines both validation and types
- Runtime safety - Catches invalid data from APIs, forms, databases
- Better errors - Zod errors are detailed and actionable
- 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()andgetProduct()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:
Was this helpful?