validated-handler

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Next.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
    any
    types or type assertions
  • ✅ 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

相关方案

  • toolchains-typescript-validation-zod
    - Zod validation patterns
  • toolchains-nextjs-core
    - Next.js App Router patterns
  • toolchains-universal-security-api-review
    - API security testing
  • universal-verification-pre-merge
    - Pre-merge verification workflows
  • toolchains-typescript-validation-zod
    - Zod验证模式
  • toolchains-nextjs-core
    - Next.js App Router模式
  • toolchains-universal-security-api-review
    - API安全测试
  • universal-verification-pre-merge
    - 预合并验证工作流