{"id":26217033,"url":"https://github.com/melvynx/next-zod-route","last_synced_at":"2025-04-15T21:50:43.798Z","repository":{"id":254820356,"uuid":"847635374","full_name":"Melvynx/next-zod-route","owner":"Melvynx","description":"Create zod-safe route for your Next.js App Directory app","archived":false,"fork":false,"pushed_at":"2025-03-27T04:13:33.000Z","size":172,"stargazers_count":19,"open_issues_count":1,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-29T01:51:39.691Z","etag":null,"topics":["api","api-route","nextjs","nextjs15","zod"],"latest_commit_sha":null,"homepage":"https://npmjs.com/next-zod-route","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Melvynx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-08-26T08:47:19.000Z","updated_at":"2025-03-25T16:28:21.000Z","dependencies_parsed_at":"2025-02-28T03:25:35.370Z","dependency_job_id":"1d177a31-db89-463d-bb37-4219f9e8992d","html_url":"https://github.com/Melvynx/next-zod-route","commit_stats":null,"previous_names":["melvynx/next-zod-route"],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Melvynx%2Fnext-zod-route","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Melvynx%2Fnext-zod-route/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Melvynx%2Fnext-zod-route/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Melvynx%2Fnext-zod-route/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Melvynx","download_url":"https://codeload.github.com/Melvynx/next-zod-route/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249161104,"owners_count":21222468,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","api-route","nextjs","nextjs15","zod"],"created_at":"2025-03-12T12:18:39.973Z","updated_at":"2025-04-15T21:50:43.790Z","avatar_url":"https://github.com/Melvynx.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003enext-zod-route\u003c/h1\u003e\n\nA fork from [next-safe-route](https://github.com/richardsolomou/next-safe-route) that uses [zod](https://github.com/colinhacks/zod) for schema validation.\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/next-zod-route\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/next-zod-route?style=for-the-badge\u0026logo=npm\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/melvynxdev/next-zod-route/actions/workflows/test.yaml\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/melvynxdev/next-zod-route/test.yaml?style=for-the-badge\u0026logo=vitest\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/melvynxdev/next-zod-route/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/npm/l/next-zod-route?style=for-the-badge\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n`next-zod-route` is a utility library for Next.js that provides type-safety and schema validation for [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers)/API Routes.\n\n## Features\n\n- **✅ Schema Validation:** Automatically validates request parameters, query strings, and body content with built-in error handling.\n- **🧷 Type-Safe:** Works with full TypeScript type safety for parameters, query strings, and body content.\n- **😌 Easy to Use:** Simple and intuitive API that makes defining route handlers a breeze.\n- **🔄 Flexible Response Handling:** Return Response objects directly or return plain objects that are automatically converted to JSON responses.\n- **🧪 Fully Tested:** Extensive test suite to ensure everything works reliably.\n- **🔐 Enhanced Middleware System:** Powerful middleware system with pre/post handler execution, response modification, and context chaining.\n- **🎯 Metadata Support:** Add and validate metadata for your routes with full type safety.\n- **🛡️ Custom Error Handling:** Flexible error handling with custom error handlers for both middleware and route handlers.\n\n## Installation\n\n```sh\nnpm install next-zod-route zod\n```\n\nOr using your preferred package manager:\n\n```sh\npnpm add next-zod-route zod\n```\n\n```sh\nyarn add next-zod-route zod\n```\n\n## Usage\n\n```ts\n// app/api/hello/route.ts\nimport { createZodRoute } from 'next-zod-route';\nimport { z } from 'zod';\n\nconst paramsSchema = z.object({\n  id: z.string(),\n});\n\nconst querySchema = z.object({\n  search: z.string().optional(),\n});\n\nconst bodySchema = z.object({\n  field: z.string(),\n});\n\nexport const GET = createZodRoute()\n  .params(paramsSchema)\n  .query(querySchema)\n  .handler((request, context) =\u003e {\n    const { id } = context.params;\n    const { search } = context.query;\n\n    return { id, search, permission, role };\n  });\n\nexport const POST = createZodRoute()\n  .params(paramsSchema)\n  .query(querySchema)\n  .body(bodySchema)\n  .handler((request, context) =\u003e {\n    // Next.js 15 use promise, but with .params we already unwrap the promise for you\n    const { id } = context.params;\n    const { search } = context.query;\n    const { field } = context.body;\n\n    // Custom status\n    return NextResponse.json({ id, search, field }), { status: 400 };\n  });\n```\n\nTo define a route handler in Next.js:\n\n1. Import `createZodRoute` and `zod`.\n2. Define validation schemas for params, query, body, and metadata as needed.\n3. Use `createZodRoute()` to create a route handler, chaining `params`, `query`, `body`, and `defineMetadata` methods.\n4. Implement your handler function, accessing validated and type-safe params, query, body, and metadata through `context`.\n\n## Supported Body Formats\n\n`next-zod-route` supports multiple request body formats out of the box:\n\n- **JSON:** Automatically parses and validates JSON bodies.\n- **URL Encoded:** Supports `application/x-www-form-urlencoded` data.\n- **Multipart Form Data:** Supports `multipart/form-data`, enabling file uploads and complex form data parsing.\n\nThe library automatically detects the content type and parses the body accordingly. For GET and DELETE requests, body parsing is skipped.\n\n## Response Handling\n\nYou can return responses in two ways:\n\n1. **Return a Response object directly:**\n\n```ts\nreturn NextResponse.json({ data: 'value' }, { status: 200 });\n```\n\n2. **Return a plain object** that will be automatically converted to a JSON response with status 200:\n\n```ts\nreturn { data: 'value' };\n```\n\n## Advanced Usage\n\n## Create client\n\nYou can create a reusable client in a file, I recommend `/src/lib/route.ts` with the following content:\n\n```tsx\nimport { createZodRoute } from 'next-zod-route';\n\nconst route = createZodRoute();\n\n// Create other re-usable route\nconst authRoute = route.use(...)\n```\n\n### Static Parameters with Metadata\n\nMetadata enable you to add **static parameters** to the route, for example to give permissions list to our application.\n\nOne powerful use case for metadata is defining required permissions for routes and checking them in middleware. This allows you to:\n\n1. Declare permissions statically at the route level\n2. Enforce permissions consistently across your application\n3. Keep authorization logic separate from your route handlers\n\nHere's how to implement permission-based authorization:\n\n```ts\n// Define a schema for permissions metadata\nconst permissionsMetadataSchema = z.object({\n  requiredPermissions: z.array(z.string()).optional(),\n});\n\n// Create a middleware that checks permissions\nconst permissionCheckMiddleware = async ({ next, metadata, request }) =\u003e {\n  // Get user permissions from auth header, token, or session\n  const userPermissions = getUserPermissions(request);\n\n  // If no required permissions in metadata, allow access\n  if (!metadata?.requiredPermissions || metadata.requiredPermissions.length === 0) {\n    return next({ context: { authorized: true } });\n  }\n\n  // Check if user has all required permissions\n  const hasAllPermissions = metadata.requiredPermissions.every((permission) =\u003e userPermissions.includes(permission));\n\n  if (!hasAllPermissions) {\n    // Short-circuit with 403 Forbidden response\n    return new Response(\n      JSON.stringify({\n        error: 'Forbidden',\n        message: 'You do not have the required permissions',\n      }),\n      {\n        status: 403,\n        headers: { 'Content-Type': 'application/json' },\n      },\n    );\n  }\n\n  // Continue with authorized context\n  return next({ context: { authorized: true } });\n};\n\n// Use in your route handlers\nexport const GET = createZodRoute()\n  .defineMetadata(permissionsMetadataSchema)\n  .use(permissionCheckMiddleware)\n  .metadata({ requiredPermissions: ['read:users'] })\n  .handler((request, context) =\u003e {\n    // Only executed if user has 'read:users' permission\n    return Response.json({ data: 'Protected data' });\n  });\n\nexport const POST = createZodRoute()\n  .defineMetadata(permissionsMetadataSchema)\n  .use(permissionCheckMiddleware)\n  .metadata({ requiredPermissions: ['write:users'] })\n  .handler((request, context) =\u003e {\n    // Only executed if user has 'write:users' permission\n    return Response.json({ success: true });\n  });\n\nexport const DELETE = createZodRoute()\n  .defineMetadata(permissionsMetadataSchema)\n  .use(permissionCheckMiddleware)\n  .metadata({ requiredPermissions: ['admin:users'] })\n  .handler((request, context) =\u003e {\n    // Only executed if user has 'admin:users' permission\n    return Response.json({ success: true });\n  });\n```\n\nThis pattern allows you to:\n\n- Clearly document required permissions for each route\n- Apply consistent authorization logic across your application\n- Skip permission checks for public routes by not specifying required permissions\n- Combine with other middleware for comprehensive request processing\n\n### Middleware\n\nYou 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.\n\n```ts\nconst loggingMiddleware = async ({ next }) =\u003e {\n  console.log('Before handler');\n  const startTime = performance.now();\n\n  const response = await next();\n\n  const endTime = performance.now() - startTime;\n  console.log(`After handler - took ${Math.round(endTime)}ms`);\n\n  return response;\n};\n\nconst authMiddleware = async ({ request, metadata, next }) =\u003e {\n  try {\n    // Get the token from the request headers\n    const token = request.headers.get('authorization')?.split(' ')[1];\n\n    // You can access metadata in middleware\n    if (metadata?.role !== 'admin') {\n      throw new Error('Unauthorized');\n    }\n\n    // Validate the token and get the user\n    const user = await validateToken(token);\n\n    // Add context \u0026 continue chain\n    const response = await next({\n      context: { user },\n    });\n\n    // You can modify the response after the handler\n    return new Response(response.body, {\n      status: response.status,\n      headers: {\n        ...Object.fromEntries(response.headers.entries()),\n        'X-User-Id': user.id,\n      },\n    });\n  } catch (error) {\n    // Errors in middleware are caught and handled by the error handler\n    throw error;\n  }\n};\n\nconst permissionsMiddleware = async ({ metadata, next }) =\u003e {\n  // Metadata are optional and type-safe\n  const response = await next({\n    context: { permissions: metadata?.permissions ?? ['read'] },\n  });\n  return response;\n};\n\nexport const GET = createZodRoute()\n  .defineMetadata(\n    z.object({\n      role: z.enum(['admin', 'user']),\n      permissions: z.array(z.string()).optional(),\n    }),\n  )\n  .use(loggingMiddleware)\n  .use(authMiddleware)\n  .use(permissionsMiddleware)\n  .handler((request, context) =\u003e {\n    // Access middleware data from context.data\n    const { user, permissions } = context.data;\n    // Access metadata from context.metadata\n    const { role } = context.metadata!;\n\n    return Response.json({ user, permissions, role });\n  });\n```\n\nMiddleware functions receive:\n\n- `request`: The request object\n- `context`: The context object with data from previous middlewares\n- `metadata`: The validated metadata object (optional)\n- `next`: Function to continue the chain and add context\n\nThe middleware can:\n\n1. Execute code before/after the handler\n2. Modify the response\n3. Add context data through the chain\n4. Short-circuit the chain by returning a Response\n5. Throw errors that will be caught by the error handler\n\n### Middleware Features\n\n#### Pre/Post Handler Execution\n\n```ts\nconst timingMiddleware = async ({ next }) =\u003e {\n  console.log('Starting request...');\n  const start = performance.now();\n\n  const response = await next();\n\n  const duration = performance.now() - start;\n  console.log(`Request took ${duration}ms`);\n\n  return response;\n};\n```\n\n#### Response Modification\n\n```ts\nconst headerMiddleware = async ({ next }) =\u003e {\n  const response = await next();\n\n  return new Response(response.body, {\n    status: response.status,\n    headers: {\n      ...Object.fromEntries(response.headers.entries()),\n      'X-Custom': 'value',\n    },\n  });\n};\n```\n\n#### Context Chaining\n\n```ts\nconst middleware1 = async ({ next }) =\u003e {\n  const response = await next({\n    context: { value1: 'first' },\n  });\n  return response;\n};\n\nconst middleware2 = async ({ context, next }) =\u003e {\n  // Access previous context\n  console.log(context.value1); // 'first'\n\n  const response = await next({\n    context: { value2: 'second' },\n  });\n  return response;\n};\n```\n\n#### Early Returns\n\n```ts\nconst authMiddleware = async ({ next }) =\u003e {\n  const isAuthed = false;\n\n  if (!isAuthed) {\n    return new Response(JSON.stringify({ error: 'Unauthorized' }), {\n      status: 401,\n      headers: { 'Content-Type': 'application/json' },\n    });\n  }\n\n  return next();\n};\n```\n\n### Migration Guide (v0.2.0)\n\nIf you're upgrading from v0.1.x to v0.2.0, there are some changes to the middleware system:\n\n#### Before (v0.1.x)\n\n```typescript\nconst authMiddleware = async () =\u003e {\n  return { user: { id: 'user-123' } };\n};\n\nconst route = createZodRoute()\n  .use(authMiddleware)\n  .handler((req, ctx) =\u003e {\n    const { user } = ctx.data;\n    return { data: user.id };\n  });\n```\n\n#### After (v0.2.0)\n\n```typescript\nconst authMiddleware = async ({ next }) =\u003e {\n  // Execute code before handler\n  console.log('Checking auth...');\n\n  // Add context \u0026 continue chain\n  const response = await next({\n    context: { user: { id: 'user-123' } },\n  });\n\n  // Modify response or execute code after\n  return new Response(response.body, {\n    headers: {\n      ...Object.fromEntries(response.headers.entries()),\n      'X-User-Id': 'user-123',\n    },\n  });\n};\n\nconst route = createZodRoute()\n  .use(authMiddleware)\n  .handler((req, ctx) =\u003e {\n    const { user } = ctx.data;\n    return { data: user.id };\n  });\n```\n\nKey changes in v0.2.0:\n\n1. Middleware must now accept an object with `request`, `context`, `metadata`, and `next`\n2. Context is passed explicitly via `next({ context: {...} })`\n3. Middleware can execute code before and after the handler\n4. Middleware can modify the response\n5. Middleware can short-circuit by returning a Response\n6. Error handling in middleware is now consistent with handler error handling\n\n### Custom Error Handler\n\nYou can specify a custom error handler function to handle errors thrown in your route handler or middleware:\n\n```ts\nimport { createZodRoute } from 'next-zod-route';\n\n// Create a custom error class\nclass CustomError extends Error {\n  constructor(\n    message: string,\n    public status: number = 400,\n  ) {\n    super(message);\n    this.name = 'CustomError';\n  }\n}\n\n// Create a route with a custom error handler\nconst safeRoute = createZodRoute({\n  handleServerError: (error: Error) =\u003e {\n    if (error instanceof CustomError) {\n      return new Response(JSON.stringify({ message: error.message }), { status: error.status });\n    }\n\n    // Default error response\n    return new Response(JSON.stringify({ message: 'Internal server error' }), { status: 500 });\n  },\n});\n\nexport const GET = safeRoute\n  .use(async () =\u003e {\n    // This error will be caught by the custom error handler\n    throw new CustomError('Middleware error', 400);\n  })\n  .handler((request, context) =\u003e {\n    // This error will also be caught by the custom error handler\n    throw new CustomError('Handler error', 400);\n  });\n```\n\nBy 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.\n\n## Validation Errors\n\nWhen validation fails, the library returns appropriate error responses:\n\n- Invalid params: `{ message: 'Invalid params' }` with status 400\n- Invalid query: `{ message: 'Invalid query' }` with status 400\n- Invalid body: `{ message: 'Invalid body' }` with status 400\n\n## Tests\n\nTests are written using [Vitest](https://vitest.dev). To run the tests, use the following command:\n\n```sh\npnpm test\n```\n\n## Contributing\n\nContributions are welcome! For major changes, please open an issue first to discuss what you would like to change.\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmelvynx%2Fnext-zod-route","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmelvynx%2Fnext-zod-route","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmelvynx%2Fnext-zod-route/lists"}