Build robust API endpoints with Next.js Route Handlers in Google Antigravity including validation authentication and error handling
# Next.js API Route Handler Patterns for Google Antigravity
API route handlers form the backend foundation of Next.js applications. This guide establishes patterns for building robust APIs with Google Antigravity, enabling Gemini 3 to generate well-structured, secure, and maintainable API endpoints.
## Base Handler Pattern
Create a reusable handler wrapper:
```typescript
// lib/api/handler.ts
import { NextRequest, NextResponse } from "next/server";
import { ZodSchema, ZodError } from "zod";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export type ApiHandler<T = unknown> = (
req: NextRequest,
context: { params: Record<string, string>; user?: { id: string; role: string } }
) => Promise<NextResponse<T>>;
type HandlerOptions = {
auth?: boolean | string[]; // true = any auth, string[] = required roles
bodySchema?: ZodSchema;
querySchema?: ZodSchema;
};
export function createHandler<T>(
handler: ApiHandler<T>,
options: HandlerOptions = {}
): (req: NextRequest, context: { params: Record<string, string> }) => Promise<NextResponse> {
return async (req, context) => {
try {
// Authentication check
if (options.auth) {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
// Role check
if (Array.isArray(options.auth)) {
if (!options.auth.includes(session.user.role)) {
return NextResponse.json(
{ error: "Forbidden" },
{ status: 403 }
);
}
}
(context as { user?: typeof session.user }).user = session.user;
}
// Body validation
if (options.bodySchema && ["POST", "PUT", "PATCH"].includes(req.method || "")) {
try {
const body = await req.json();
options.bodySchema.parse(body);
} catch (error) {
if (error instanceof ZodError) {
return NextResponse.json(
{ error: "Validation failed", details: error.errors },
{ status: 400 }
);
}
throw error;
}
}
// Execute handler
return await handler(req, context as Parameters<ApiHandler<T>>[1]);
} catch (error) {
console.error("API Error:", error);
if (error instanceof ApiError) {
return NextResponse.json(
{ error: error.message },
{ status: error.statusCode }
);
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
};
}
export class ApiError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = "ApiError";
}
}
```
## CRUD Route Implementation
Implement RESTful endpoints:
```typescript
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createHandler, ApiError } from "@/lib/api/handler";
import { prisma } from "@/lib/prisma";
import { createPostSchema, postQuerySchema } from "@/lib/validations/post";
// GET /api/posts
export const GET = createHandler(async (req) => {
const { searchParams } = new URL(req.url);
const query = postQuerySchema.parse(Object.fromEntries(searchParams));
const posts = await prisma.post.findMany({
where: {
published: true,
...(query.category && { categoryId: query.category }),
...(query.search && {
OR: [
{ title: { contains: query.search, mode: "insensitive" } },
{ content: { contains: query.search, mode: "insensitive" } },
],
}),
},
include: {
author: { select: { id: true, name: true, avatarUrl: true } },
_count: { select: { comments: true, likes: true } },
},
orderBy: { createdAt: "desc" },
skip: (query.page - 1) * query.limit,
take: query.limit,
});
const total = await prisma.post.count({ where: { published: true } });
return NextResponse.json({
posts,
pagination: {
page: query.page,
limit: query.limit,
total,
totalPages: Math.ceil(total / query.limit),
},
});
});
// POST /api/posts
export const POST = createHandler(
async (req, { user }) => {
const body = await req.json();
const data = createPostSchema.parse(body);
const post = await prisma.post.create({
data: {
...data,
authorId: user!.id,
slug: generateSlug(data.title),
},
include: {
author: { select: { id: true, name: true } },
},
});
return NextResponse.json(post, { status: 201 });
},
{ auth: true, bodySchema: createPostSchema }
);
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createHandler, ApiError } from "@/lib/api/handler";
import { prisma } from "@/lib/prisma";
import { updatePostSchema } from "@/lib/validations/post";
// GET /api/posts/:id
export const GET = createHandler(async (req, { params }) => {
const post = await prisma.post.findUnique({
where: { id: params.id },
include: {
author: { select: { id: true, name: true, avatarUrl: true } },
comments: {
include: { author: { select: { id: true, name: true } } },
orderBy: { createdAt: "desc" },
},
},
});
if (!post) {
throw new ApiError(404, "Post not found");
}
return NextResponse.json(post);
});
// PATCH /api/posts/:id
export const PATCH = createHandler(
async (req, { params, user }) => {
const body = await req.json();
const data = updatePostSchema.parse(body);
const post = await prisma.post.findUnique({ where: { id: params.id } });
if (!post) throw new ApiError(404, "Post not found");
if (post.authorId !== user!.id && user!.role !== "admin") {
throw new ApiError(403, "Not authorized");
}
const updated = await prisma.post.update({
where: { id: params.id },
data,
});
return NextResponse.json(updated);
},
{ auth: true }
);
// DELETE /api/posts/:id
export const DELETE = createHandler(
async (req, { params, user }) => {
const post = await prisma.post.findUnique({ where: { id: params.id } });
if (!post) throw new ApiError(404, "Post not found");
if (post.authorId !== user!.id && user!.role !== "admin") {
throw new ApiError(403, "Not authorized");
}
await prisma.post.delete({ where: { id: params.id } });
return new NextResponse(null, { status: 204 });
},
{ auth: true }
);
```
## Best Practices
1. **Centralized error handling**: Use wrapper functions
2. **Input validation**: Validate all inputs with Zod
3. **Authentication middleware**: Check auth before processing
4. **Consistent responses**: Use standard response formats
5. **Rate limiting**: Protect endpoints from abuseThis Next.js prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their next.js implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For Next.js projects, consider mentioning your framework version, coding style, and any specific libraries you're using.