diff --git a/apps/example/src/app/api/v2/route-with-path-params/[slug]/route.ts b/apps/example/src/app/api/v2/route-with-path-params/[slug]/route.ts new file mode 100644 index 0000000..faa748d --- /dev/null +++ b/apps/example/src/app/api/v2/route-with-path-params/[slug]/route.ts @@ -0,0 +1,30 @@ +import { TypedNextResponse, route, routeOperation } from 'next-rest-framework'; +import { z } from 'zod'; + +const paramsSchema = z.object({ + slug: z.enum(['foo', 'bar', 'baz']) +}); + +export const runtime = 'edge'; + +export const { GET } = route({ + getPathParams: routeOperation({ + method: 'GET' + }) + .input({ + contentType: 'application/json', + params: paramsSchema + }) + .outputs([ + { + status: 200, + contentType: 'application/json', + body: paramsSchema + } + ]) + .handler((_req, { params: { slug } }) => { + return TypedNextResponse.json({ + slug + }); + }) +}); diff --git a/apps/example/src/app/api/v2/route-with-query-params/route.ts b/apps/example/src/app/api/v2/route-with-query-params/route.ts index f3248d5..084d326 100644 --- a/apps/example/src/app/api/v2/route-with-query-params/route.ts +++ b/apps/example/src/app/api/v2/route-with-query-params/route.ts @@ -2,9 +2,7 @@ import { TypedNextResponse, route, routeOperation } from 'next-rest-framework'; import { z } from 'zod'; const querySchema = z.object({ - foo: z.string().uuid(), - bar: z.string().optional(), - baz: z.string() + total: z.string() }); export const runtime = 'edge'; @@ -28,9 +26,7 @@ export const { GET } = route({ const query = req.nextUrl.searchParams; return TypedNextResponse.json({ - foo: query.get('foo') ?? '', - bar: query.get('bar') ?? '', - baz: query.get('baz') ?? '' + total: query.get('total') ?? '' }); }) }); diff --git a/apps/example/src/pages/api/v1/route-with-path-params/[slug]/index.ts b/apps/example/src/pages/api/v1/route-with-path-params/[slug]/index.ts new file mode 100644 index 0000000..72a483e --- /dev/null +++ b/apps/example/src/pages/api/v1/route-with-path-params/[slug]/index.ts @@ -0,0 +1,26 @@ +import { apiRoute, apiRouteOperation } from 'next-rest-framework'; +import { z } from 'zod'; + +const paramsSchema = z.object({ + slug: z.enum(['foo', 'bar', 'baz']) +}); + +export default apiRoute({ + getQueryParams: apiRouteOperation({ + method: 'GET' + }) + .input({ + contentType: 'application/json', + query: paramsSchema + }) + .outputs([ + { + status: 200, + contentType: 'application/json', + body: paramsSchema + } + ]) + .handler((req, res) => { + res.json(req.query); + }) +}); diff --git a/apps/example/src/pages/api/v1/route-with-query-params/index.ts b/apps/example/src/pages/api/v1/route-with-query-params/index.ts index f122c4d..ed278e5 100644 --- a/apps/example/src/pages/api/v1/route-with-query-params/index.ts +++ b/apps/example/src/pages/api/v1/route-with-query-params/index.ts @@ -2,9 +2,7 @@ import { apiRoute, apiRouteOperation } from 'next-rest-framework'; import { z } from 'zod'; const querySchema = z.object({ - foo: z.string().uuid(), - bar: z.string().optional(), - baz: z.string() + total: z.string() }); export default apiRoute({ diff --git a/docs/docs/api-reference.md b/docs/docs/api-reference.md index 4e0fbcf..15eb742 100644 --- a/docs/docs/api-reference.md +++ b/docs/docs/api-reference.md @@ -57,7 +57,7 @@ The route operation input function is used for type-checking, validation and doc | `bodySchema` | A JSON schema that you can provide in case the conversion of the `body` Zod schema fails or produces an incorrect result in your OpenAPI spec. | `false` | | `query` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the query parameters. When the query schema is defined, a request with invalid query parameters will get an error response. Query parameters are parsed using this schema and updated to the request if valid, so the query parameters from the request should always match the schema. | `false` | | `querySchema` | A JSON schema that you can provide in case the conversion of the `query` Zod schema fails or produces an incorrect result in your OpenAPI spec. | `false` | -| `params` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the path parameters for strong typing when using them in your route handler. | `false` | +| `params` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the path parameters. When the params schema is defined, a request with invalid path parameters will get an error response. Path parameters are parsed using this schema and updated to the request if valid, so the path parameters from the request should always match the schema. | `false` | Calling the route operation input function allows you to chain your API handler logic with the [Route operation outputs](#route-operation-outputs), [Route operation middleware](#route-operation-middleware) and [Route operation handler](#route-operation-handler) functions. diff --git a/packages/next-rest-framework/README.md b/packages/next-rest-framework/README.md index 0205223..5c0d730 100644 --- a/packages/next-rest-framework/README.md +++ b/packages/next-rest-framework/README.md @@ -736,7 +736,7 @@ The route operation input function is used for type-checking, validation and doc | `bodySchema` | A JSON schema that you can provide in case the conversion of the `body` Zod schema fails or produces an incorrect result in your OpenAPI spec. | `false` | | `query` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the query parameters. When the query schema is defined, a request with invalid query parameters will get an error response. Query parameters are parsed using this schema and updated to the request if valid, so the query parameters from the request should always match the schema. | `false` | | `querySchema` | A JSON schema that you can provide in case the conversion of the `query` Zod schema fails or produces an incorrect result in your OpenAPI spec. | `false` | -| `params` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the path parameters for strong typing when using them in your route handler. | `false` | +| `params` | A [Zod](https://github.com/colinhacks/zod) schema describing the format of the path parameters. When the params schema is defined, a request with invalid path parameters will get an error response. Path parameters are parsed using this schema and updated to the request if valid, so the path parameters from the request should always match the schema. | `false` | Calling the route operation input function allows you to chain your API handler logic with the [Route operation outputs](#route-operation-outputs), [Route operation middleware](#route-operation-middleware) and [Route operation handler](#route-operation-handler) functions. diff --git a/packages/next-rest-framework/src/app-router/route.ts b/packages/next-rest-framework/src/app-router/route.ts index fc9bd9a..ee60399 100644 --- a/packages/next-rest-framework/src/app-router/route.ts +++ b/packages/next-rest-framework/src/app-router/route.ts @@ -49,7 +49,11 @@ export const route = >( const { input, handler, middleware1, middleware2, middleware3 } = operation; - let reqClone = req.clone() as NextRequest; + let reqClone = new NextRequest(req.url, { + method: req.method, + headers: req.headers, + body: req.body + }); let middlewareOptions: BaseOptions = {}; @@ -94,7 +98,8 @@ export const route = >( const { body: bodySchema, query: querySchema, - contentType: contentTypeSchema + contentType: contentTypeSchema, + params: paramsSchema } = input; const contentType = req.headers.get('content-type')?.split(';')[0]; @@ -109,7 +114,7 @@ export const route = >( if (bodySchema) { if (contentType === 'application/json') { try { - const json = await req.clone().json(); + const json = await reqClone.json(); const { valid, errors, data } = validateSchema({ schema: bodySchema, @@ -129,11 +134,12 @@ export const route = >( } reqClone = new NextRequest(reqClone.url, { - ...reqClone, method: reqClone.method, headers: reqClone.headers, body: JSON.stringify(data) }); + + reqClone.json = async () => data; } catch { return NextResponse.json( { @@ -150,7 +156,7 @@ export const route = >( FORM_DATA_CONTENT_TYPES.includes(contentType as FormDataContentType) ) { try { - const formData = await req.clone().formData(); + const formData = await reqClone.formData(); const { valid, errors, data } = validateSchema({ schema: bodySchema, @@ -171,7 +177,6 @@ export const route = >( // Inject parsed for data to JSON body. reqClone = new NextRequest(reqClone.url, { - ...reqClone, method: reqClone.method, headers: reqClone.headers, body: JSON.stringify(data) @@ -203,7 +208,9 @@ export const route = >( if (querySchema) { const { valid, errors, data } = validateSchema({ schema: querySchema, - obj: qs.parse(req.nextUrl.search, { ignoreQueryPrefix: true }) + obj: qs.parse(reqClone.nextUrl.search, { + ignoreQueryPrefix: true + }) }); if (!valid) { @@ -230,9 +237,38 @@ export const route = >( }); reqClone = new NextRequest(url, { - ...reqClone, method: reqClone.method, - headers: reqClone.headers + headers: reqClone.headers, + body: reqClone.body + }); + } + + if (paramsSchema) { + const { valid, errors, data } = validateSchema({ + schema: paramsSchema, + obj: context.params + }); + + if (!valid) { + return NextResponse.json( + { + message: DEFAULT_ERRORS.invalidPathParameters, + errors + }, + { + status: 400 + } + ); + } + + context.params = data; + const url = new URL(reqClone.url); + url.search = new URLSearchParams(context.params).toString(); + + reqClone = new NextRequest(url, { + method: reqClone.method, + headers: reqClone.headers, + body: reqClone.body }); } } diff --git a/packages/next-rest-framework/src/constants.ts b/packages/next-rest-framework/src/constants.ts index 7baf818..cb75f85 100644 --- a/packages/next-rest-framework/src/constants.ts +++ b/packages/next-rest-framework/src/constants.ts @@ -9,7 +9,8 @@ export const DEFAULT_ERRORS = { operationNotAllowed: 'Operation not allowed.', invalidRequestBody: 'Invalid request body.', missingRequestBody: 'Missing request body.', - invalidQueryParameters: 'Invalid query parameters.' + invalidQueryParameters: 'Invalid query parameters.', + invalidPathParameters: 'Invalid path parameters.' }; export enum ValidMethod {