All files / src/lib/api/middleware withValidation.ts

55.29% Statements 47/85
100% Branches 0/0
0% Functions 0/2
55.29% Lines 47/85

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 861x 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                    
/**
 * Validation Middleware
 *
 * Validates request body against a Zod schema.
 */
 
import { NextRequest, NextResponse } from 'next/server';
import { ZodSchema } from 'zod';
import { validationErrorResponse, type ApiErrorResponse } from '../responses';
import type { RouteContext, ValidatedHandler } from './types';
 
/**
 * Validation middleware
 *
 * Wraps a handler to validate the request body against a Zod schema.
 * If validation fails, returns a 400 error with validation details.
 *
 * @example
 * ```ts
 * const createProductSchema = z.object({
 *   title: z.string().min(1),
 *   price: z.number().positive(),
 * });
 *
 * export const POST = withValidation(createProductSchema)(
 *   async (request, context, data) => {
 *     // data is typed and validated
 *     const product = await prisma.product.create({ data });
 *     return createdResponse(product);
 *   }
 * );
 * ```
 */
export function withValidation<V>(schema: ZodSchema<V>) {
  return <T>(handler: ValidatedHandler<T, V>) => {
    return async (
      request: Request,
      context?: RouteContext
    ): Promise<NextResponse<T | ApiErrorResponse>> => {
      let body: unknown;

      try {
        body = await request.json();
      } catch {
        return validationErrorResponse('Invalid JSON body');
      }

      const result = schema.safeParse(body);

      if (!result.success) {
        const details = result.error.issues.map((issue) => ({
          field: issue.path.join('.') || '_root',
          message: issue.message,
          code: issue.code }));

        return validationErrorResponse('Validation failed', details);
      }

      return handler(request as NextRequest, context, result.data);
    };
  };
}
 
/**
 * Parse request body as JSON (without validation)
 *
 * @example
 * ```ts
 * export const POST = async (request) => {
 *   const body = await parseBody(request);
 *   if (body.error) return body.error;
 *   // body.data is the parsed JSON
 * };
 * ```
 */
export async function parseBody<T = unknown>(
  request: Request
): Promise<{ data: T; error?: never } | { data?: never; error: NextResponse }> {
  try {
    const data = (await request.json()) as T;
    return { data };
  } catch {
    return { error: validationErrorResponse('Invalid JSON body') };
  }
}