Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | /**
* Request Validation Middleware
*
* Provides HOC for validating API route requests.
*/
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
interface ValidationOptions {
body?: z.ZodType;
query?: z.ZodType;
params?: z.ZodType;
}
type ValidatedData<T extends ValidationOptions> = {
body: T['body'] extends z.ZodType ? z.infer<T['body']> : undefined;
query: T['query'] extends z.ZodType ? z.infer<T['query']> : undefined;
params: T['params'] extends z.ZodType ? z.infer<T['params']> : undefined;
};
/**
* HOC for validating request data
*
* @example
* ```ts
* export const POST = withValidation(
* {
* body: z.object({ name: z.string() }),
* query: z.object({ page: z.number() }),
* },
* async (request, { body, query }) => {
* // body and query are validated and typed
* return NextResponse.json({ success: true });
* }
* );
* ```
*/
export function withValidation<T extends ValidationOptions>(
options: T,
handler: (
request: NextRequest,
validated: ValidatedData<T>,
context?: { params: Promise<Record<string, string>> }
) => Promise<NextResponse>
) {
return async (
request: NextRequest,
context?: { params: Promise<Record<string, string>> }
): Promise<NextResponse> => {
try {
const validated: Record<string, unknown> = {
body: undefined,
query: undefined,
params: undefined,
};
// Validate body for non-GET requests
if (options.body && request.method !== 'GET' && request.method !== 'HEAD') {
try {
const body = await request.json();
validated.body = options.body.parse(body);
} catch (error) {
if (error instanceof z.ZodError) {
return createValidationErrorResponse(error);
}
return NextResponse.json(
{ error: 'Invalid JSON body' },
{ status: 400 }
);
}
}
// Validate query parameters
if (options.query) {
const searchParams = request.nextUrl.searchParams;
const query = parseSearchParams(searchParams);
try {
validated.query = options.query.parse(query);
} catch (error) {
if (error instanceof z.ZodError) {
return createValidationErrorResponse(error);
}
throw error;
}
}
// Validate route parameters
if (options.params && context?.params) {
try {
const params = await context.params;
validated.params = options.params.parse(params);
} catch (error) {
if (error instanceof z.ZodError) {
return createValidationErrorResponse(error);
}
throw error;
}
}
return handler(request, validated as ValidatedData<T>, context);
} catch (error) {
if (error instanceof z.ZodError) {
return createValidationErrorResponse(error);
}
throw error;
}
};
}
/**
* Parse URLSearchParams into an object
*/
function parseSearchParams(searchParams: URLSearchParams): Record<string, string | string[]> {
const result: Record<string, string | string[]> = {};
searchParams.forEach((value, key) => {
const existing = result[key];
if (existing !== undefined) {
if (Array.isArray(existing)) {
existing.push(value);
} else {
result[key] = [existing, value];
}
} else {
result[key] = value;
}
});
return result;
}
/**
* Create a standardized validation error response
*/
function createValidationErrorResponse(error: z.ZodError): NextResponse {
return NextResponse.json(
{
error: 'Validation failed',
details: error.issues.map((e: z.ZodIssue) => ({
field: e.path.join('.'),
message: e.message,
code: e.code,
})),
},
{ status: 400 }
);
}
/**
* Validate and extract a single field from request
*/
export async function validateField<T extends z.ZodType>(
request: NextRequest,
field: string,
schema: T
): Promise<z.infer<T>> {
const body = await request.json();
return schema.parse(body[field]);
}
/**
* Create a typed params validator for route handlers
*/
export function createParamsValidator<T extends z.ZodType>(schema: T) {
return async (params: Promise<Record<string, string>>): Promise<z.infer<T>> => {
const resolvedParams = await params;
return schema.parse(resolvedParams);
};
}
|