Gathering detailed insights and metrics for next-zod-route
Gathering detailed insights and metrics for next-zod-route
Gathering detailed insights and metrics for next-zod-route
Gathering detailed insights and metrics for next-zod-route
next-typesafe-url
Fully typesafe, JSON serializable, and zod validated URL search params, dynamic route params, and routing for NextJS pages directory
next-api-zod
This is a simple wrapper for API route [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) in the new (standard now) App Router to validate user input.
@wzulfikar/next-zod-route-fork
A zod way to define route handlers in Next.js
next-zod-validation
Zod schema validations for Next.js route handlers
npm install next-zod-route
Typescript
Module System
Min. Node Version
Node Version
NPM Version
TypeScript (98.84%)
JavaScript (1.02%)
Shell (0.14%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
52 Stars
93 Commits
9 Forks
3 Watchers
3 Branches
5 Contributors
Updated on Jul 08, 2025
Latest Version
0.2.6
Package Id
next-zod-route@0.2.6
Unpacked Size
67.40 kB
Size
13.49 kB
File Count
9
NPM Version
10.9.0
Node Version
22.12.0
Published on
May 20, 2025
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
1
A fork from next-safe-route that uses zod for schema validation.
next-zod-route
is a utility library for Next.js that provides type-safety and schema validation for Route Handlers/API Routes.
1npm install next-zod-route zod
Or using your preferred package manager:
1pnpm add next-zod-route zod
1yarn add next-zod-route zod
1// app/api/hello/route.ts 2import { createZodRoute } from 'next-zod-route'; 3import { z } from 'zod'; 4 5const paramsSchema = z.object({ 6 id: z.string(), 7}); 8 9const querySchema = z.object({ 10 search: z.string().optional(), 11}); 12 13const bodySchema = z.object({ 14 field: z.string(), 15}); 16 17export const GET = createZodRoute() 18 .params(paramsSchema) 19 .query(querySchema) 20 .handler((request, context) => { 21 const { id } = context.params; 22 const { search } = context.query; 23 24 return { id, search, permission, role }; 25 }); 26 27export const POST = createZodRoute() 28 .params(paramsSchema) 29 .query(querySchema) 30 .body(bodySchema) 31 .handler((request, context) => { 32 // Next.js 15 use promise, but with .params we already unwrap the promise for you 33 const { id } = context.params; 34 const { search } = context.query; 35 const { field } = context.body; 36 37 // Custom status 38 return NextResponse.json({ id, search, field }), { status: 400 }; 39 });
To define a route handler in Next.js:
createZodRoute
and zod
.createZodRoute()
to create a route handler, chaining params
, query
, body
, and defineMetadata
methods.context
.next-zod-route
supports multiple request body formats out of the box:
application/x-www-form-urlencoded
data.multipart/form-data
, enabling file uploads and complex form data parsing.The library automatically detects the content type and parses the body accordingly. For GET and DELETE requests, body parsing is skipped.
You can return responses in two ways:
1return NextResponse.json({ data: 'value' }, { status: 200 });
1return { data: 'value' };
You can create a reusable client in a file, I recommend /src/lib/route.ts
with the following content:
1import { createZodRoute } from 'next-zod-route'; 2 3const route = createZodRoute(); 4 5// Create other re-usable route 6const authRoute = route.use(...)
Metadata enable you to add static parameters to the route, for example to give permissions list to our application.
One powerful use case for metadata is defining required permissions for routes and checking them in middleware. This allows you to:
Here's how to implement permission-based authorization:
1// Define a schema for permissions metadata 2const permissionsMetadataSchema = z.object({ 3 requiredPermissions: z.array(z.string()).optional(), 4}); 5 6// Create a middleware that checks permissions 7const permissionCheckMiddleware = async ({ next, metadata, request }) => { 8 // Get user permissions from auth header, token, or session 9 const userPermissions = getUserPermissions(request); 10 11 // If no required permissions in metadata, allow access 12 if (!metadata?.requiredPermissions || metadata.requiredPermissions.length === 0) { 13 return next({ context: { authorized: true } }); 14 } 15 16 // Check if user has all required permissions 17 const hasAllPermissions = metadata.requiredPermissions.every((permission) => userPermissions.includes(permission)); 18 19 if (!hasAllPermissions) { 20 // Short-circuit with 403 Forbidden response 21 return new Response( 22 JSON.stringify({ 23 error: 'Forbidden', 24 message: 'You do not have the required permissions', 25 }), 26 { 27 status: 403, 28 headers: { 'Content-Type': 'application/json' }, 29 }, 30 ); 31 } 32 33 // Continue with authorized context 34 return next({ context: { authorized: true } }); 35}; 36 37// Use in your route handlers 38export const GET = createZodRoute() 39 .defineMetadata(permissionsMetadataSchema) 40 .use(permissionCheckMiddleware) 41 .metadata({ requiredPermissions: ['read:users'] }) 42 .handler((request, context) => { 43 // Only executed if user has 'read:users' permission 44 return Response.json({ data: 'Protected data' }); 45 }); 46 47export const POST = createZodRoute() 48 .defineMetadata(permissionsMetadataSchema) 49 .use(permissionCheckMiddleware) 50 .metadata({ requiredPermissions: ['write:users'] }) 51 .handler((request, context) => { 52 // Only executed if user has 'write:users' permission 53 return Response.json({ success: true }); 54 }); 55 56export const DELETE = createZodRoute() 57 .defineMetadata(permissionsMetadataSchema) 58 .use(permissionCheckMiddleware) 59 .metadata({ requiredPermissions: ['admin:users'] }) 60 .handler((request, context) => { 61 // Only executed if user has 'admin:users' permission 62 return Response.json({ success: true }); 63 });
This pattern allows you to:
You can add middleware to your route handler with the use
method. Middleware functions can add data to the context that will be available in your handler.
1const loggingMiddleware = async ({ next }) => { 2 console.log('Before handler'); 3 const startTime = performance.now(); 4 5 const response = await next(); 6 7 const endTime = performance.now() - startTime; 8 console.log(`After handler - took ${Math.round(endTime)}ms`); 9 10 return response; 11}; 12 13const authMiddleware = async ({ request, metadata, next }) => { 14 try { 15 // Get the token from the request headers 16 const token = request.headers.get('authorization')?.split(' ')[1]; 17 18 // You can access metadata in middleware 19 if (metadata?.role !== 'admin') { 20 throw new Error('Unauthorized'); 21 } 22 23 // Validate the token and get the user 24 const user = await validateToken(token); 25 26 // Add context & continue chain 27 const response = await next({ 28 context: { user }, 29 }); 30 31 // You can modify the response after the handler 32 return new Response(response.body, { 33 status: response.status, 34 headers: { 35 ...Object.fromEntries(response.headers.entries()), 36 'X-User-Id': user.id, 37 }, 38 }); 39 } catch (error) { 40 // Errors in middleware are caught and handled by the error handler 41 throw error; 42 } 43}; 44 45const permissionsMiddleware = async ({ metadata, next }) => { 46 // Metadata are optional and type-safe 47 const response = await next({ 48 context: { permissions: metadata?.permissions ?? ['read'] }, 49 }); 50 return response; 51}; 52 53export const GET = createZodRoute() 54 .defineMetadata( 55 z.object({ 56 role: z.enum(['admin', 'user']), 57 permissions: z.array(z.string()).optional(), 58 }), 59 ) 60 .use(loggingMiddleware) 61 .use(authMiddleware) 62 .use(permissionsMiddleware) 63 .handler((request, context) => { 64 // Access middleware data from context.data 65 const { user, permissions } = context.data; 66 // Access metadata from context.metadata 67 const { role } = context.metadata!; 68 69 return Response.json({ user, permissions, role }); 70 });
Middleware functions receive:
request
: The request objectcontext
: The context object with data from previous middlewaresmetadata
: The validated metadata object (optional)next
: Function to continue the chain and add contextThe middleware can:
1const timingMiddleware = async ({ next }) => { 2 console.log('Starting request...'); 3 const start = performance.now(); 4 5 const response = await next(); 6 7 const duration = performance.now() - start; 8 console.log(`Request took ${duration}ms`); 9 10 return response; 11};
1const headerMiddleware = async ({ next }) => {
2 const response = await next();
3
4 return new Response(response.body, {
5 status: response.status,
6 headers: {
7 ...Object.fromEntries(response.headers.entries()),
8 'X-Custom': 'value',
9 },
10 });
11};
1const middleware1 = async ({ next }) => { 2 const response = await next({ 3 context: { value1: 'first' }, 4 }); 5 return response; 6}; 7 8const middleware2 = async ({ context, next }) => { 9 // Access previous context 10 console.log(context.value1); // 'first' 11 12 const response = await next({ 13 context: { value2: 'second' }, 14 }); 15 return response; 16};
1const authMiddleware = async ({ next }) => { 2 const isAuthed = false; 3 4 if (!isAuthed) { 5 return new Response(JSON.stringify({ error: 'Unauthorized' }), { 6 status: 401, 7 headers: { 'Content-Type': 'application/json' }, 8 }); 9 } 10 11 return next(); 12};
If you're upgrading from v0.1.x to v0.2.0, there are some changes to the middleware system:
1const authMiddleware = async () => { 2 return { user: { id: 'user-123' } }; 3}; 4 5const route = createZodRoute() 6 .use(authMiddleware) 7 .handler((req, ctx) => { 8 const { user } = ctx.data; 9 return { data: user.id }; 10 });
1const authMiddleware = async ({ next }) => { 2 // Execute code before handler 3 console.log('Checking auth...'); 4 5 // Add context & continue chain 6 const response = await next({ 7 context: { user: { id: 'user-123' } }, 8 }); 9 10 // Modify response or execute code after 11 return new Response(response.body, { 12 headers: { 13 ...Object.fromEntries(response.headers.entries()), 14 'X-User-Id': 'user-123', 15 }, 16 }); 17}; 18 19const route = createZodRoute() 20 .use(authMiddleware) 21 .handler((req, ctx) => { 22 const { user } = ctx.data; 23 return { data: user.id }; 24 });
Key changes in v0.2.0:
request
, context
, metadata
, and next
next({ context: {...} })
You can specify a custom error handler function to handle errors thrown in your route handler or middleware:
1import { createZodRoute } from 'next-zod-route'; 2 3// Create a custom error class 4class CustomError extends Error { 5 constructor( 6 message: string, 7 public status: number = 400, 8 ) { 9 super(message); 10 this.name = 'CustomError'; 11 } 12} 13 14// Create a route with a custom error handler 15const safeRoute = createZodRoute({ 16 handleServerError: (error: Error) => { 17 if (error instanceof CustomError) { 18 return new Response(JSON.stringify({ message: error.message }), { status: error.status }); 19 } 20 21 // Default error response 22 return new Response(JSON.stringify({ message: 'Internal server error' }), { status: 500 }); 23 }, 24}); 25 26export const GET = safeRoute 27 .use(async () => { 28 // This error will be caught by the custom error handler 29 throw new CustomError('Middleware error', 400); 30 }) 31 .handler((request, context) => { 32 // This error will also be caught by the custom error handler 33 throw new CustomError('Handler error', 400); 34 });
By default, if no custom error handler is provided, the library will return a generic "Internal server error" message with a 500 status code to avoid information leakage.
When validation fails, the library returns appropriate error responses:
{ message: 'Invalid params' }
with status 400{ message: 'Invalid query' }
with status 400{ message: 'Invalid body' }
with status 400Tests are written using Vitest. To run the tests, use the following command:
1pnpm test
Contributions are welcome! For major changes, please open an issue first to discuss what you would like to change.
This project is licensed under the MIT License - see the LICENSE file for details.
No vulnerabilities found.
No security vulnerabilities found.