Introduction
Managing customer reviews is crucial for trust and SEO. In 2026, systems must include strict validation, automated moderation, and performant display. This tutorial guides you step by step in creating a complete solution with Next.js App Router, Prisma, and TypeScript. You will learn to secure submissions, calculate average ratings, and optimize queries for thousands of reviews.
Prerequisites
- Next.js 15 with TypeScript
- Node.js 20+
- Prisma 5.20+
- PostgreSQL database
- Solid knowledge of React and API routes
Prisma Schema for Reviews
model Review {
id Int @id @default(autoincrement())
rating Int
comment String @db.Text
approved Boolean @default(false)
userId String
productId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
product Product @relation(fields: [productId], references: [id])
}This schema defines the Review model with rating validation, moderation, and relations. The approved field enables manual moderation before public display.
POST API Route for Submitting a Review
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { z } from 'zod';
const reviewSchema = z.object({
rating: z.number().min(1).max(5),
comment: z.string().min(10).max(500),
productId: z.string(),
});
export async function POST(req: NextRequest) {
const body = await req.json();
const parsed = reviewSchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: 'Invalid data' }, { status: 400 });
const { rating, comment, productId } = parsed.data;
const review = await prisma.review.create({
data: { rating, comment, productId, userId: 'user-123', approved: false }
});
return NextResponse.json(review, { status: 201 });
}This route uses Zod to validate incoming data and creates the review in unapproved mode by default. It prevents invalid submissions and injections.
GET API Route for Approved Reviews
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const productId = searchParams.get('productId');
if (!productId) return NextResponse.json({ error: 'Missing productId' }, { status: 400 });
const reviews = await prisma.review.findMany({
where: { productId, approved: true },
orderBy: { createdAt: 'desc' },
take: 20
});
return NextResponse.json(reviews);
}This query optimizes performance by filtering only approved reviews and limiting results. Ideal for client-side display.
Review Stars Display Component
import React from 'react';
interface Props { rating: number; }
export default function ReviewStars({ rating }: Props) {
return (
<div className="flex">
{Array.from({ length: 5 }).map((_, i) => (
<span key={i} className={i < rating ? 'text-yellow-500' : 'text-gray-300'}>★</span>
))}
</div>
);
}Reusable and accessible component that visually displays the rating. It is optimized for server-side rendering and avoids unnecessary re-renders.
Average Rating Calculation Function
import { prisma } from './prisma';
export async function getAverageRating(productId: string): Promise<number> {
const result = await prisma.review.aggregate({
where: { productId, approved: true },
_avg: { rating: true }
});
return result._avg.rating || 0;
}Uses Prisma aggregation to calculate the average in the database, avoiding loading all reviews into memory. Highly performant at scale.
Best Practices
- Always validate inputs server-side with Zod
- Moderate reviews before publication
- Use indexes on productId and approved
- Limit the number of reviews returned per query
- Protect routes with authentication
Common Mistakes to Avoid
- Forgetting validation and allowing invalid ratings
- Not separating creation and moderation
- Loading all reviews without pagination
- Ignoring SQL injections through poorly constructed dynamic queries
Going Further
Discover our advanced Next.js and e-commerce system courses.