validated-handler
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.js Validated Handler Pattern
Next.js 验证处理器模式
Type-safe API route handler with automatic Zod validation for Next.js App Router.
适用于Next.js App Router的类型安全API路由处理器,具备自动Zod验证功能。
When to Use This Skill
何时使用该方案
Use this skill when:
- Building Next.js API routes (App Router)
- Want automatic input validation with Zod
- Need consistent error handling across API routes
- Want to eliminate boilerplate validation code
- Building type-safe APIs with TypeScript
在以下场景使用该方案:
- 构建Next.js API路由(App Router)
- 希望通过Zod实现自动输入验证
- 需要在所有API路由中实现一致的错误处理
- 希望消除重复的验证样板代码
- 使用TypeScript构建类型安全的API
The Problem
存在的问题
Without a validated handler, every API route has repetitive validation code:
typescript
// ❌ REPETITIVE - Every route looks like this
export async function GET(request: NextRequest) {
try {
// 1. Parse query params
const { searchParams } = new URL(request.url);
const rawPage = searchParams.get('page');
const rawLimit = searchParams.get('limit');
// 2. Validate each param manually
if (!rawPage || isNaN(Number(rawPage))) {
return NextResponse.json(
{ error: 'Invalid page parameter' },
{ status: 400 }
);
}
if (!rawLimit || isNaN(Number(rawLimit))) {
return NextResponse.json(
{ error: 'Invalid limit parameter' },
{ status: 400 }
);
}
const page = Number(rawPage);
const limit = Number(rawLimit);
// 3. Validate ranges
if (page < 1) {
return NextResponse.json(
{ error: 'Page must be >= 1' },
{ status: 400 }
);
}
if (limit < 1 || limit > 100) {
return NextResponse.json(
{ error: 'Limit must be between 1 and 100' },
{ status: 400 }
);
}
// 4. Finally, business logic
const data = await fetchData(page, limit);
return NextResponse.json(data);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}Problems:
- 30+ lines of boilerplate per route
- Error-prone manual validation
- Inconsistent error messages
- No type safety
- Hard to maintain across 100+ routes
如果没有验证处理器,每个API路由都会包含重复的验证代码:
typescript
// ❌ 重复代码 - 每个路由都是如此
export async function GET(request: NextRequest) {
try {
// 1. 解析查询参数
const { searchParams } = new URL(request.url);
const rawPage = searchParams.get('page');
const rawLimit = searchParams.get('limit');
// 2. 手动验证每个参数
if (!rawPage || isNaN(Number(rawPage))) {
return NextResponse.json(
{ error: 'Invalid page parameter' },
{ status: 400 }
);
}
if (!rawLimit || isNaN(Number(rawLimit))) {
return NextResponse.json(
{ error: 'Invalid limit parameter' },
{ status: 400 }
);
}
const page = Number(rawPage);
const limit = Number(rawLimit);
// 3. 验证范围
if (page < 1) {
return NextResponse.json(
{ error: 'Page must be >= 1' },
{ status: 400 }
);
}
if (limit < 1 || limit > 100) {
return NextResponse.json(
{ error: 'Limit must be between 1 and 100' },
{ status: 400 }
);
}
// 4. 最终处理业务逻辑
const data = await fetchData(page, limit);
return NextResponse.json(data);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}问题:
- 每个路由需要30行以上的样板代码
- 手动验证容易出错
- 错误消息不一致
- 缺乏类型安全
- 在100+个路由中难以维护
The Solution: validatedHandler
解决方案:validatedHandler
Create a reusable handler that combines Zod validation with Next.js API routes:
typescript
// src/lib/api/handler.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
type ValidationSource = 'query' | 'body';
export function validatedHandler<T extends z.ZodType>(
config: {
input: { schema: T; source: ValidationSource };
},
handler: (ctx: { input: z.infer<T>; request: NextRequest }) => Promise<Response>,
) {
return async (request: NextRequest): Promise<Response> => {
try {
// 1. Parse input based on source
const rawInput = config.input.source === 'query'
? Object.fromEntries(new URL(request.url).searchParams)
: await request.json();
// 2. Validate with Zod schema
const result = config.input.schema.safeParse(rawInput);
if (!result.success) {
return NextResponse.json({
error: "Validation failed",
details: result.error.issues.map(err => ({
path: err.path.join("."),
message: err.message,
})),
}, { status: 400 });
}
// 3. Call handler with typed data
return await handler({ input: result.data, request });
} catch (error) {
console.error('API Error:', error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
};
}创建一个可复用的处理器,将Zod验证与Next.js API路由相结合:
typescript
// src/lib/api/handler.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
type ValidationSource = 'query' | 'body';
export function validatedHandler<T extends z.ZodType>(
config: {
input: { schema: T; source: ValidationSource };
},
handler: (ctx: { input: z.infer<T>; request: NextRequest }) => Promise<Response>,
) {
return async (request: NextRequest): Promise<Response> => {
try {
// 1. 根据来源解析输入
const rawInput = config.input.source === 'query'
? Object.fromEntries(new URL(request.url).searchParams)
: await request.json();
// 2. 通过Zod Schema验证
const result = config.input.schema.safeParse(rawInput);
if (!result.success) {
return NextResponse.json({
error: "Validation failed",
details: result.error.issues.map(err => ({
path: err.path.join("."),
message: err.message,
})),
}, { status: 400 });
}
// 3. 传入类型化数据调用处理器
return await handler({ input: result.data, request });
} catch (error) {
console.error('API Error:', error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
};
}Usage Example
使用示例
With validatedHandler, routes become clean and type-safe:
typescript
// src/app/api/schools/route.ts
import { validatedHandler } from '@/lib/api/handler';
import { paginationInputSchema } from '@/lib/api/pagination';
import { z } from 'zod';
import { db } from '@/lib/db';
import { schools } from '@/lib/db/schema';
import { ilike } from 'drizzle-orm';
// Define schema
const getSchoolsSchema = paginationInputSchema.extend({
keyword: z.string().optional(),
districtId: z.string().uuid().optional(),
});
// Use validatedHandler - clean and type-safe!
export const GET = validatedHandler({
input: { source: 'query', schema: getSchoolsSchema }
}, async ({ input }) => {
// input is fully typed: { page: number, limit: number, keyword?: string, districtId?: string }
const schoolList = await db.query.schools.findMany({
where: input.keyword
? ilike(schools.name, `%${input.keyword}%`)
: undefined,
limit: input.limit,
offset: (input.page - 1) * input.limit,
});
return NextResponse.json(schoolList);
});Benefits:
- ✅ Only 15 lines vs 50+ lines
- ✅ Automatic validation with Zod
- ✅ Full TypeScript type inference
- ✅ Consistent error responses
- ✅ No manual parsing
- ✅ Single place to maintain validation logic
使用validatedHandler后,路由变得简洁且类型安全:
typescript
// src/app/api/schools/route.ts
import { validatedHandler } from '@/lib/api/handler';
import { paginationInputSchema } from '@/lib/api/pagination';
import { z } from 'zod';
import { db } from '@/lib/db';
import { schools } from '@/lib/db/schema';
import { ilike } from 'drizzle-orm';
// 定义Schema
const getSchoolsSchema = paginationInputSchema.extend({
keyword: z.string().optional(),
districtId: z.string().uuid().optional(),
});
// 使用validatedHandler - 简洁且类型安全!
export const GET = validatedHandler({
input: { source: 'query', schema: getSchoolsSchema }
}, async ({ input }) => {
// input是完全类型化的: { page: number, limit: number, keyword?: string, districtId?: string }
const schoolList = await db.query.schools.findMany({
where: input.keyword
? ilike(schools.name, `%${input.keyword}%`)
: undefined,
limit: input.limit,
offset: (input.page - 1) * input.limit,
});
return NextResponse.json(schoolList);
});优势:
- ✅ 仅需15行代码(原需50+行)
- ✅ 通过Zod实现自动验证
- ✅ 完整的TypeScript类型推断
- ✅ 一致的错误响应
- ✅ 无需手动解析
- ✅ 验证逻辑集中维护
Core Implementation
核心实现
Complete Handler Implementation
完整处理器实现
typescript
// src/lib/api/handler.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
type ValidationSource = 'query' | 'body';
interface HandlerConfig<T extends z.ZodType> {
input: {
schema: T;
source: ValidationSource;
};
}
interface HandlerContext<T extends z.ZodType> {
input: z.infer<T>;
request: NextRequest;
}
export function validatedHandler<T extends z.ZodType>(
config: HandlerConfig<T>,
handler: (ctx: HandlerContext<T>) => Promise<Response>,
) {
return async (request: NextRequest): Promise<Response> => {
try {
// Parse input based on source
let rawInput: unknown;
if (config.input.source === 'query') {
const { searchParams } = new URL(request.url);
rawInput = Object.fromEntries(searchParams);
} else if (config.input.source === 'body') {
rawInput = await request.json();
}
// Validate with Zod
const result = config.input.schema.safeParse(rawInput);
if (!result.success) {
return NextResponse.json({
error: "Validation failed",
details: result.error.issues.map(err => ({
path: err.path.join("."),
message: err.message,
})),
}, { status: 400 });
}
// Call handler with typed data
return await handler({
input: result.data,
request,
});
} catch (error) {
// Log error for debugging
console.error('API Error:', error);
// Return generic error to client
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
};
}typescript
// src/lib/api/handler.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
type ValidationSource = 'query' | 'body';
interface HandlerConfig<T extends z.ZodType> {
input: {
schema: T;
source: ValidationSource;
};
}
interface HandlerContext<T extends z.ZodType> {
input: z.infer<T>;
request: NextRequest;
}
export function validatedHandler<T extends z.ZodType>(
config: HandlerConfig<T>,
handler: (ctx: HandlerContext<T>) => Promise<Response>,
) {
return async (request: NextRequest): Promise<Response> => {
try {
// 根据来源解析输入
let rawInput: unknown;
if (config.input.source === 'query') {
const { searchParams } = new URL(request.url);
rawInput = Object.fromEntries(searchParams);
} else if (config.input.source === 'body') {
rawInput = await request.json();
}
// 通过Zod验证
const result = config.input.schema.safeParse(rawInput);
if (!result.success) {
return NextResponse.json({
error: "Validation failed",
details: result.error.issues.map(err => ({
path: err.path.join("."),
message: err.message,
})),
}, { status: 400 });
}
// 传入类型化数据调用处理器
return await handler({
input: result.data,
request,
});
} catch (error) {
// 记录错误用于调试
console.error('API Error:', error);
// 向客户端返回通用错误
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
};
}Pagination Schema (Reusable)
分页Schema(可复用)
typescript
// src/lib/api/pagination.ts
import { z } from 'zod';
export const paginationInputSchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(10),
});
export type PaginationInput = z.infer<typeof paginationInputSchema>;
export type PaginatedResponse<T> = {
data: T[];
page: number;
limit: number;
total: number;
totalPages: number;
nextPage: number | null;
previousPage: number | null;
};
export function createPaginatedResponse<T>(
data: T[],
total: number,
page: number,
limit: number
): PaginatedResponse<T> {
const totalPages = Math.ceil(total / limit);
return {
data,
page,
limit,
total,
totalPages,
nextPage: page < totalPages ? page + 1 : null,
previousPage: page > 1 ? page - 1 : null,
};
}typescript
// src/lib/api/pagination.ts
import { z } from 'zod';
export const paginationInputSchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(10),
});
export type PaginationInput = z.infer<typeof paginationInputSchema>;
export type PaginatedResponse<T> = {
data: T[];
page: number;
limit: number;
total: number;
totalPages: number;
nextPage: number | null;
previousPage: number | null;
};
export function createPaginatedResponse<T>(
data: T[],
total: number,
page: number,
limit: number
): PaginatedResponse<T> {
const totalPages = Math.ceil(total / limit);
return {
data,
page,
limit,
total,
totalPages,
nextPage: page < totalPages ? page + 1 : null,
previousPage: page > 1 ? page - 1 : null,
};
}Common Patterns
常见模式
GET Route with Query Params
带查询参数的GET路由
typescript
// src/app/api/providers/route.ts
import { validatedHandler } from '@/lib/api/handler';
import { paginationInputSchema } from '@/lib/api/pagination';
import { z } from 'zod';
const getProvidersSchema = paginationInputSchema.extend({
status: z.enum(['active', 'inactive']).optional(),
specialty: z.string().optional(),
});
export const GET = validatedHandler({
input: { source: 'query', schema: getProvidersSchema }
}, async ({ input }) => {
const providers = await db.query.providers.findMany({
where: buildWhereClause(input),
limit: input.limit,
offset: (input.page - 1) * input.limit,
});
return NextResponse.json(providers);
});typescript
// src/app/api/providers/route.ts
import { validatedHandler } from '@/lib/api/handler';
import { paginationInputSchema } from '@/lib/api/pagination';
import { z } from 'zod';
const getProvidersSchema = paginationInputSchema.extend({
status: z.enum(['active', 'inactive']).optional(),
specialty: z.string().optional(),
});
export const GET = validatedHandler({
input: { source: 'query', schema: getProvidersSchema }
}, async ({ input }) => {
const providers = await db.query.providers.findMany({
where: buildWhereClause(input),
limit: input.limit,
offset: (input.page - 1) * input.limit,
});
return NextResponse.json(providers);
});POST Route with Body Validation
带Body验证的POST路由
typescript
// src/app/api/providers/route.ts
import { validatedHandler } from '@/lib/api/handler';
import { z } from 'zod';
const createProviderSchema = z.object({
name: z.string().min(1).max(255),
email: z.string().email(),
specialty: z.string().min(1),
licenseNumber: z.string().optional(),
});
export const POST = validatedHandler({
input: { source: 'body', schema: createProviderSchema }
}, async ({ input }) => {
const newProvider = await db.insert(providers)
.values(input)
.returning();
return NextResponse.json(newProvider[0], { status: 201 });
});typescript
// src/app/api/providers/route.ts
import { validatedHandler } from '@/lib/api/handler';
import { z } from 'zod';
const createProviderSchema = z.object({
name: z.string().min(1).max(255),
email: z.string().email(),
specialty: z.string().min(1),
licenseNumber: z.string().optional(),
});
export const POST = validatedHandler({
input: { source: 'body', schema: createProviderSchema }
}, async ({ input }) => {
const newProvider = await db.insert(providers)
.values(input)
.returning();
return NextResponse.json(newProvider[0], { status: 201 });
});Route with Path Parameters
带路径参数的路由
typescript
// src/app/api/providers/[id]/route.ts
import { validatedHandler } from '@/lib/api/handler';
import { z } from 'zod';
const updateProviderSchema = z.object({
name: z.string().min(1).max(255).optional(),
email: z.string().email().optional(),
specialty: z.string().min(1).optional(),
});
export const PATCH = validatedHandler({
input: { source: 'body', schema: updateProviderSchema }
}, async ({ input, request }) => {
// Extract path param manually
const url = new URL(request.url);
const id = url.pathname.split('/').pop();
if (!id) {
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
}
const updated = await db.update(providers)
.set(input)
.where(eq(providers.id, id))
.returning();
return NextResponse.json(updated[0]);
});typescript
// src/app/api/providers/[id]/route.ts
import { validatedHandler } from '@/lib/api/handler';
import { z } from 'zod';
const updateProviderSchema = z.object({
name: z.string().min(1).max(255).optional(),
email: z.string().email().optional(),
specialty: z.string().min(1).optional(),
});
export const PATCH = validatedHandler({
input: { source: 'body', schema: updateProviderSchema }
}, async ({ input, request }) => {
// 手动提取路径参数
const url = new URL(request.url);
const id = url.pathname.split('/').pop();
if (!id) {
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
}
const updated = await db.update(providers)
.set(input)
.where(eq(providers.id, id))
.returning();
return NextResponse.json(updated[0]);
});Route with Authentication
带身份验证的路由
typescript
// src/app/api/providers/route.ts
import { validatedHandler } from '@/lib/api/handler';
import { auth } from '@/lib/auth';
import { z } from 'zod';
const getProvidersSchema = paginationInputSchema.extend({
status: z.enum(['active', 'inactive']).optional(),
});
export const GET = validatedHandler({
input: { source: 'query', schema: getProvidersSchema }
}, async ({ input, request }) => {
// Authentication check
const session = await auth(request);
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Authorization check
if (!session.user.roles.includes('admin')) {
return NextResponse.json(
{ error: 'Forbidden' },
{ status: 403 }
);
}
// Business logic
const providers = await db.query.providers.findMany({
where: buildWhereClause(input),
limit: input.limit,
offset: (input.page - 1) * input.limit,
});
return NextResponse.json(providers);
});typescript
// src/app/api/providers/route.ts
import { validatedHandler } from '@/lib/api/handler';
import { auth } from '@/lib/auth';
import { z } from 'zod';
const getProvidersSchema = paginationInputSchema.extend({
status: z.enum(['active', 'inactive']).optional(),
});
export const GET = validatedHandler({
input: { source: 'query', schema: getProvidersSchema }
}, async ({ input, request }) => {
// 身份验证检查
const session = await auth(request);
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// 权限验证检查
if (!session.user.roles.includes('admin')) {
return NextResponse.json(
{ error: 'Forbidden' },
{ status: 403 }
);
}
// 业务逻辑
const providers = await db.query.providers.findMany({
where: buildWhereClause(input),
limit: input.limit,
offset: (input.page - 1) * input.limit,
});
return NextResponse.json(providers);
});Advanced Patterns
进阶模式
Multiple Validation Sources
多验证来源
typescript
// Validate both query and body
const searchSchema = z.object({
query: z.string(),
});
const filtersSchema = z.object({
category: z.string().optional(),
priceMin: z.number().optional(),
priceMax: z.number().optional(),
});
export const POST = async (request: NextRequest) => {
// Validate query params
const queryResult = searchSchema.safeParse(
Object.fromEntries(new URL(request.url).searchParams)
);
if (!queryResult.success) {
return NextResponse.json({ error: 'Invalid query' }, { status: 400 });
}
// Validate body
const body = await request.json();
const bodyResult = filtersSchema.safeParse(body);
if (!bodyResult.success) {
return NextResponse.json({ error: 'Invalid filters' }, { status: 400 });
}
// Use both
const results = await search(queryResult.data.query, bodyResult.data);
return NextResponse.json(results);
};typescript
// 同时验证查询参数和请求体
const searchSchema = z.object({
query: z.string(),
});
const filtersSchema = z.object({
category: z.string().optional(),
priceMin: z.number().optional(),
priceMax: z.number().optional(),
});
export const POST = async (request: NextRequest) => {
// 验证查询参数
const queryResult = searchSchema.safeParse(
Object.fromEntries(new URL(request.url).searchParams)
);
if (!queryResult.success) {
return NextResponse.json({ error: 'Invalid query' }, { status: 400 });
}
// 验证请求体
const body = await request.json();
const bodyResult = filtersSchema.safeParse(body);
if (!bodyResult.success) {
return NextResponse.json({ error: 'Invalid filters' }, { status: 400 });
}
// 同时使用两者
const results = await search(queryResult.data.query, bodyResult.data);
return NextResponse.json(results);
};Custom Error Responses
自定义错误响应
typescript
// src/lib/api/handler.ts (enhanced)
export function validatedHandler<T extends z.ZodType>(
config: {
input: { schema: T; source: ValidationSource };
errorTransform?: (error: z.ZodError) => { error: string; details?: unknown };
},
handler: (ctx: { input: z.infer<T>; request: NextRequest }) => Promise<Response>,
) {
return async (request: NextRequest): Promise<Response> => {
try {
const rawInput = config.input.source === 'query'
? Object.fromEntries(new URL(request.url).searchParams)
: await request.json();
const result = config.input.schema.safeParse(rawInput);
if (!result.success) {
const errorResponse = config.errorTransform
? config.errorTransform(result.error)
: {
error: "Validation failed",
details: result.error.issues.map(err => ({
path: err.path.join("."),
message: err.message,
})),
};
return NextResponse.json(errorResponse, { status: 400 });
}
return await handler({ input: result.data, request });
} catch (error) {
console.error('API Error:', error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
};
}typescript
// src/lib/api/handler.ts(增强版)
export function validatedHandler<T extends z.ZodType>(
config: {
input: { schema: T; source: ValidationSource };
errorTransform?: (error: z.ZodError) => { error: string; details?: unknown };
},
handler: (ctx: { input: z.infer<T>; request: NextRequest }) => Promise<Response>,
) {
return async (request: NextRequest): Promise<Response> => {
try {
const rawInput = config.input.source === 'query'
? Object.fromEntries(new URL(request.url).searchParams)
: await request.json();
const result = config.input.schema.safeParse(rawInput);
if (!result.success) {
const errorResponse = config.errorTransform
? config.errorTransform(result.error)
: {
error: "Validation failed",
details: result.error.issues.map(err => ({
path: err.path.join("."),
message: err.message,
})),
};
return NextResponse.json(errorResponse, { status: 400 });
}
return await handler({ input: result.data, request });
} catch (error) {
console.error('API Error:', error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
};
}Testing
测试
Unit Tests for Handler
处理器单元测试
typescript
// src/lib/api/handler.test.ts
import { validatedHandler } from './handler';
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
describe('validatedHandler', () => {
it('should validate query params successfully', async () => {
const schema = z.object({
page: z.coerce.number(),
});
const handler = validatedHandler({
input: { source: 'query', schema }
}, async ({ input }) => {
return NextResponse.json({ page: input.page });
});
const request = new NextRequest('http://localhost?page=2');
const response = await handler(request);
const data = await response.json();
expect(data).toEqual({ page: 2 });
});
it('should return 400 for invalid input', async () => {
const schema = z.object({
page: z.coerce.number().min(1),
});
const handler = validatedHandler({
input: { source: 'query', schema }
}, async ({ input }) => {
return NextResponse.json({ page: input.page });
});
const request = new NextRequest('http://localhost?page=0');
const response = await handler(request);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toBe('Validation failed');
});
});typescript
// src/lib/api/handler.test.ts
import { validatedHandler } from './handler';
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
describe('validatedHandler', () => {
it('should validate query params successfully', async () => {
const schema = z.object({
page: z.coerce.number(),
});
const handler = validatedHandler({
input: { source: 'query', schema }
}, async ({ input }) => {
return NextResponse.json({ page: input.page });
});
const request = new NextRequest('http://localhost?page=2');
const response = await handler(request);
const data = await response.json();
expect(data).toEqual({ page: 2 });
});
it('should return 400 for invalid input', async () => {
const schema = z.object({
page: z.coerce.number().min(1),
});
const handler = validatedHandler({
input: { source: 'query', schema }
}, async ({ input }) => {
return NextResponse.json({ page: input.page });
});
const request = new NextRequest('http://localhost?page=0');
const response = await handler(request);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toBe('Validation failed');
});
});Integration Tests for API Routes
API路由集成测试
typescript
// src/app/api/schools/route.test.ts
import { GET } from './route';
import { NextRequest } from 'next/server';
describe('GET /api/schools', () => {
it('should return paginated schools', async () => {
const request = new NextRequest('http://localhost/api/schools?page=1&limit=10');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveProperty('data');
expect(data).toHaveProperty('page', 1);
expect(data).toHaveProperty('limit', 10);
});
it('should validate pagination parameters', async () => {
const request = new NextRequest('http://localhost/api/schools?page=-1');
const response = await GET(request);
expect(response.status).toBe(400);
});
});typescript
// src/app/api/schools/route.test.ts
import { GET } from './route';
import { NextRequest } from 'next/server';
describe('GET /api/schools', () => {
it('should return paginated schools', async () => {
const request = new NextRequest('http://localhost/api/schools?page=1&limit=10');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveProperty('data');
expect(data).toHaveProperty('page', 1);
expect(data).toHaveProperty('limit', 10);
});
it('should validate pagination parameters', async () => {
const request = new NextRequest('http://localhost/api/schools?page=-1');
const response = await GET(request);
expect(response.status).toBe(400);
});
});Benefits Summary
优势总结
Code Reduction
代码精简
- Before: 50+ lines per route with validation
- After: 10-15 lines per route
- Savings: 70% code reduction
- 之前: 每个路由需50+行验证代码
- 之后: 每个路由仅需10-15行代码
- 节省: 代码量减少70%
Type Safety
类型安全
- ✅ Input types automatically inferred from Zod schema
- ✅ No types or type assertions
any - ✅ Compile-time validation of schema usage
- ✅ 输入类型从Zod Schema自动推断
- ✅ 无类型或类型断言
any - ✅ 在编译阶段验证Schema的使用
Developer Experience
开发者体验
- ✅ Single place to define validation
- ✅ Consistent error messages
- ✅ Clear separation of validation and business logic
- ✅ Easy to test
- ✅ 验证逻辑集中定义
- ✅ 一致的错误消息
- ✅ 验证逻辑与业务逻辑清晰分离
- ✅ 易于测试
Maintainability
可维护性
- ✅ DRY principle applied
- ✅ Changes to validation logic in one place
- ✅ Reusable schemas across routes
- ✅ Framework-agnostic pattern (works with Express, Fastify, Hono)
- ✅ 遵循DRY原则
- ✅ 验证逻辑的修改只需在一处进行
- ✅ Schema可在多个路由中复用
- ✅ 与框架无关的模式(适用于Express、Fastify、Hono)
Pattern Variations
模式变种
For Express.js/Fastify
适用于Express.js/Fastify
typescript
export function validatedHandler<T extends z.ZodType>(
schema: T,
handler: (input: z.infer<T>, req: Request, res: Response) => Promise<void>
) {
return async (req: Request, res: Response) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
await handler(result.data, req, res);
};
}typescript
export function validatedHandler<T extends z.ZodType>(
schema: T,
handler: (input: z.infer<T>, req: Request, res: Response) => Promise<void>
) {
return async (req: Request, res: Response) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
await handler(result.data, req, res);
};
}For Hono
适用于Hono
typescript
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
const app = new Hono();
app.get('/schools', zValidator('query', getSchoolsSchema), async (c) => {
const input = c.req.valid('query');
const schools = await fetchSchools(input);
return c.json(schools);
});typescript
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
const app = new Hono();
app.get('/schools', zValidator('query', getSchoolsSchema), async (c) => {
const input = c.req.valid('query');
const schools = await fetchSchools(input);
return c.json(schools);
});Related Skills
相关方案
- - Zod validation patterns
toolchains-typescript-validation-zod - - Next.js App Router patterns
toolchains-nextjs-core - - API security testing
toolchains-universal-security-api-review - - Pre-merge verification workflows
universal-verification-pre-merge
- - Zod验证模式
toolchains-typescript-validation-zod - - Next.js App Router模式
toolchains-nextjs-core - - API安全测试
toolchains-universal-security-api-review - - 预合并验证工作流
universal-verification-pre-merge