Skip to content

Conversation

@sdangol
Copy link
Contributor

@sdangol sdangol commented Nov 11, 2025

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 createValidationMiddleware function that validates request and response data using Standard Schema-compliant libraries (Zod, Valibot, ArkType)
  • Validation Option: Added validation option 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) and ResponseValidationError (500)
  • Type Safety: Full TypeScript support with Standard Schema types

Implementation Details

New Files:

  • packages/event-handler/src/rest/middleware/validation.ts - Validation middleware implementation
  • packages/event-handler/src/rest/errors.ts - Added RequestValidationError and ResponseValidationError classes
  • packages/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 integration
  • packages/event-handler/src/types/rest.ts - Added validation types and options
  • examples/snippets/event-handler/rest/validation_*.ts - Example code snippets
  • docs/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

  • Read the Contributing Guidelines
  • Added tests that prove the change is effective
  • Added/updated documentation
  • Added code examples
  • PR title follows conventional commit semantics
  • 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.

@pull-request-size pull-request-size bot added the size/XXL PRs with 1K+ LOC, largely documentation related label Nov 11, 2025
@sonarqubecloud
Copy link

@svozza
Copy link
Contributor

svozza commented Nov 12, 2025

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' };
});

@dreamorosi
Copy link
Contributor

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:

  1. 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.
  2. 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.

// Validate request
if (reqSchemas) {
if (reqSchemas.body) {
let bodyData: unknown = reqCtx.event.body;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid using the raw event in middleware because they are specific to the particualr lambda integration. This means that the middleware may only work with say API Gateway or function URLs etc. We should use the req and res objects where bossible. Bearing in mind that if we wish to use the body or request or a response, we need to clone it.

path: Path,
middlewareOrHandler?: Middleware[] | RouteHandler,
handler?: RouteHandler
handlerOrOptions?: RouteHandler | Omit<RestRouteOptions, 'method' | 'path'>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in #4736 (comment) we won't need to change anything in all these HTTP methods if we treat validation as just another middleware.

const contentType = response.headers.get('content-type');

let bodyData: unknown;
if (contentType?.includes('application/json')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified:

const bodyData = contentType?.includes('application/json') 
  ? await clonedResponse.json() : await clonedResponse.text();;

schema: StandardSchemaV1,
data: unknown,
component: 'body' | 'headers' | 'path' | 'query',
isRequest: boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic booleans in APIs are a code smell imo.

/**
* Configuration for request validation
*/
type RequestValidationConfig<T = unknown> = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems odd to me that these are all optional. Is there a way to express through the type system that at least one of these should be present?

@svozza
Copy link
Contributor

svozza commented Nov 13, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XXL PRs with 1K+ LOC, largely documentation related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: first class support for data validation in Event Handler

3 participants