feat(event-handler): add validation support for REST router
Summary
[!NOTE] This PR is an experimental PR generated using the AI-DLC workflow with the model Claude Sonnet 4.5
Changes
This PR adds first-class support for data validation in the Event Handler REST router using the Standard Schema specification
Issue number: closes #4516
What's Changed
- Validation Middleware: Added
createValidationMiddlewarefunction that validates request and response data using Standard Schema-compliant libraries (Zod, Valibot, ArkType) - Validation Option: Added
validationoption to route configuration for declarative validation - Request Validation: Validates request body, headers, path parameters, and query parameters
- Response Validation: Validates response body and headers using Web Response API
- Error Handling: Automatic error handling with
RequestValidationError(422) andResponseValidationError(500) - Type Safety: Full TypeScript support with Standard Schema types
AIDLC Files
https://gist.github.com/sdangol/f0652110b583bac6854805e7d3e592e9
Implementation Details
New Files:
packages/event-handler/src/rest/middleware/validation.ts- Validation middleware implementationpackages/event-handler/src/rest/errors.ts- AddedRequestValidationErrorandResponseValidationErrorclassespackages/event-handler/tests/unit/rest/middleware/validation.test.ts- Comprehensive test suite (16 tests)packages/event-handler/tests/unit/rest/validation-errors.test.ts- Error class tests (16 tests)
Modified Files:
packages/event-handler/src/rest/Router.ts- Added validation middleware integrationpackages/event-handler/src/types/rest.ts- Added validation types and optionsexamples/snippets/event-handler/rest/validation_*.ts- Example code snippetsdocs/features/event-handler/rest.md- Documentation for validation feature
API Usage
import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest';
import { z } from 'zod';
const app = new Router();
// Validate request body
app.post('/users', async () => {
return { id: '123', name: 'John' };
}, {
validation: { req: { body: z.object({ name: z.string() }) } },
});
// Validate request and response
app.get('/users/:id', async (reqCtx) => {
return { id: reqCtx.params.id, name: 'John' };
}, {
validation: {
req: { path: z.object({ id: z.string().uuid() }) },
res: { body: z.object({ id: z.string(), name: z.string() }) },
},
});
Testing
- 32 tests covering validation middleware and error classes
- 97.4% code coverage for validation middleware
- All tests use spies to verify validation execution
- Tests cover request/response validation for body, headers, path, and query parameters
Breaking Changes
None - this is a new feature addition.
Checklist
- [x] Read the Contributing Guidelines
- [x] Added tests that prove the change is effective
- [x] Added/updated documentation
- [x] Added code examples
- [x] PR title follows conventional commit semantics
- [x] Change meets project tenets (minimal abstraction, progressive disclosure)
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
Given that this is just middleware under the hood, I think we can simplify the API here. We already allow to users to pass middleware into their routes, I don't see a reason to make the type signature of the HTTP verb methods more complicated when we can do this instead:
import {validation} from '@aws-lambda-powertools/event-handler/experimental-rest/middleware';
app.post('/users', [validation({ req: { body: z.object({ name: z.string() }) } })], async () => {
return { id: '123', name: 'John' };
});
This was implemented this way because of the RFC.
It's true that it's just a middleware, but I'd like us to at least consider the option of having a special treatment for this one.
I'm suggesting this because of two aspects:
- We want whatever schemas are defined there to strongly type the handler and its arguments. I am not sure how to write a generic that picks the
z.infer<>types from an array of unknown middleware functions and transposes it to the handler parameter. Having a named argument like the PR suggests makes it a bit more tenable. - This is mainly stylistic, but considering that this feature is expected to be the foundation for the OpenAPI schema one, these schemas might become quite large. Having them mixed into a middleware array and above the handler can make things a bit messy to read. Instead, putting them under a named argument might be a bit better.
Arguably the first one might be a skill issue on my side, if there's a way to mitigate it, happy to remove the concern. For the second, it's primarily preference so again, I just wanted to have a discussion, not trying to force any other direction.
Also @sdangol, I'd consider moving all the AIDLC files into a public gist and linking that one in the PR body, the sheer amount of markdown generated is a bit unwieldy when it comes to looking at the diff.
I'll answer your points in reverse order @dreamorosi.
This is mainly stylistic, but considering that this feature is expected to be the foundation for the OpenAPI schema one, these schemas might become quite large. Having them mixed into a middleware array and above the handler can make things a bit messy to read. Instead, putting them under a named argument might be a bit better.
I wouldn't recommend embedding a large validation, espeically OpenAPI, in the handler signature either way. I would have a separate place for the validation definitions, something like this:
// validators.ts
import {validation} from '@aws-lambda-powertools/event-handler/experimental-rest/middleware';
export userValidation = validiation(/** very large validation object **/);
// index.ts
import {userValidation} from './validators.js';
app.post('/users', [userValidation], async () => {
return { id: '123', name: 'John' };
});
We want whatever schemas are defined there to strongly type the handler and its arguments. I am not sure how to write a generic that picks the
z.infer<>types from an array of unknown middleware functions and transposes it to the handler parameter. Having a named argument like the PR suggests makes it a bit more tenable.
While I would be disappointed to see the simplicity of treating everything as middleware go, this is a very compelling argument. Trying to infer types from things that may or may not be in an array sounds like a nightmare, especially when the elements in that array are just functions.
My main objection to adding this extra argument is the compelxity it creates because we now have these HTTP methods that can take, 2, 3 or 4 arguments depending on the use case. This is also compounded when you throw decorators into the mix. I think maybe the way forward then is to have a options object so that we can reduce the combinations of arguments. This way the function will only ever have 2 or 3 arguments.
app.post('/users', async () => {
return { id: '123', name: 'John' };
}, {middleware: [ /** ... */], validation: {/** ... */} });
I don't love the way the middleware is after the handler here but having the optional argument last simplifies things more imo. Obviously this is a decision that needs to be made before GA as it is a breaking change.
After a discussion with @sdangol I've decided we should keep the proposed signature in this PR:
app.post('/users', [[ /** middleware */]], async () => {
return { id: '123', name: 'John' };
}, {validation: {/** ... */} });
This is on the condition that this function will no have any more arguments added to it, any new additions will always go in to the final options object where the validation field is. I will write this up formally in the validation implementation issue I am going to write this week.
I updated the issue with more requirements a few days ago: https://github.com/aws-powertools/powertools-lambda-typescript/issues/4516.
@svozza I've updated the PR now addressing the updated requirements.
Quality Gate passed
Issues
0 New issues
0 Accepted issues
Measures
0 Security Hotspots
0.0% Coverage on New Code
6.4% Duplication on New Code