Skip to content

Comments

feat(event-handler): add validation support for REST router#4736

Open
sdangol wants to merge 52 commits intomainfrom
aidlc
Open

feat(event-handler): add validation support for REST router#4736
sdangol wants to merge 52 commits intomainfrom
aidlc

Conversation

@sdangol
Copy link
Contributor

@sdangol sdangol commented Nov 11, 2025

Edit (by @svozza): I have updated the description to reflect the current state of this PR. Any comments before 2026-02-19 refer to the previous implementation.

Summary

This PR adds request and response validation middleware to the HTTP event handler, along with type-safe route handler support and extended response body handling.

Closes #4516


Changes

Validation middleware

A new validate() middleware validates request and response data against Standard Schema v1 schemas. Standard Schema is a vendor-neutral interface supported by Zod, Valibot, ArkType, and other popular validation libraries.

You can validate any combination of request body, headers, path parameters, and query parameters. Request validation errors return a structured 422 Unprocessable Entity response. Response validation errors return 500 Internal Server Error.

When you provide a validation config, TypeScript infers the handler's context type automatically. The handler receives a valid property containing the parsed and typed data: only the fields you configured schemas for are accessible. As such, accessing an unvalidated field (e.g., reqCtx.valid.req.headers when no headers schema was configured) is a compile-time error:

import { z } from 'zod';

const bodySchema = z.object({ name: z.string() });

app.post('/users', (reqCtx) => {
  const { name } = reqCtx.valid.req.body; // typed as { name: string }
  // reqCtx.valid.req.headers             // compile error — no headers schema configured
  return { id: 1, name };
}, {
  validation: {
    req: { body: bodySchema },
  },
});

When a response schema is also configured, reqCtx.valid.res becomes available in post-handler middleware. Without a response schema, valid.res does not exist on the type:

const resSchema = z.object({ id: z.number(), name: z.string() });

app.post('/users', (reqCtx) => {
  const { name } = reqCtx.valid.req.body;
  // reqCtx.valid.res                     // compile error — only available when res schema is configured
  return { id: 1, name };                 // checked against resSchema at runtime
}, {
  validation: {
    req: { body: bodySchema },
    res: { body: resSchema },
  },
});

You can also pass validation alongside a middleware array:

app.post('/users', [authMiddleware], handler, {
  validation: {
    req: { body: bodySchema },
  },
});

Type-safe route handlers

This provides compile-time checking only — TypeScript raises an error if the handler returns a shape that does not match User, but no runtime validation takes place. To validate the response shape at compile time and runtime, use the res option in the validation config instead:

const resSchema = z.object({ id: z.number(), name: z.string() });

app.get('/users', () => {
  return { id: 1, name: 'Alice' }; // validated at runtime against resSchema
}, {
  validation: { res: { body: resSchema } },
});

You can also combine request validation with a compile-time response type by annotating the handler's return type directly:

app.post('/users', (reqCtx): User => {
  const { name } = reqCtx.valid.req.body;
  return { id: 1, name }; // TypeScript enforces the return shape here
}, { validation: { req: { body: bodySchema } } });

New error classes

Class Status
RequestValidationError 422 Unprocessable Entity
ResponseValidationError 500 Internal Server Error

Both classes extend HttpError and include structured issue details in the response body.

Extended response body

ExtendedAPIGatewayProxyResult now accepts any JSON-serialisable value as the body field, and correctly handles responses with no body (such as 204 No Content). Previously, only string, ArrayBuffer, and streams were accepted, and omitting body would cause the response object itself to be serialised as JSON with a 200 status.

// JSON object body — previously required JSON.stringify()
app.get('/users', () => ({
  statusCode: 200,
  body: { id: 1, name: 'Alice' },
}));

// JSON array body
app.get('/items', () => ({
  statusCode: 200,
  body: [1, 2, 3],
}));

// No body
app.delete('/users/:id', () => ({
  statusCode: 204,
}));

New dependency

@standard-schema/spec is added as an optional peer dependency. It provides only TypeScript types and a small interface definition with no runtime overhead, and is only required when using the validate middleware.

Testing


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
@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.

@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.

@svozza
Copy link
Contributor

svozza commented Nov 19, 2025

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.

@svozza
Copy link
Contributor

svozza commented Dec 4, 2025

I updated the issue with more requirements a few days ago: #4516.

@boring-cyborg boring-cyborg bot added dependencies Changes that touch dependencies, e.g. Dependabot, etc. documentation Improvements or additions to documentation event-handler This item relates to the Event Handler Utility tests PRs that add or change tests labels Dec 6, 2025
@sdangol sdangol marked this pull request as ready for review December 10, 2025 11:43
@sdangol
Copy link
Contributor Author

sdangol commented Dec 10, 2025

@svozza I've updated the PR now addressing the updated requirements.

Copy link
Contributor Author

@sdangol sdangol left a comment

Choose a reason for hiding this comment

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

@svozza Added some comments to highlight

@dreamorosi dreamorosi marked this pull request as draft December 17, 2025 15:43
@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 5, 2026

@svozza svozza force-pushed the aidlc branch 3 times, most recently from 55f2f73 to 27ba4bb Compare February 18, 2026 01:05
…ed proxy result

- Replace "coming soon" validation section with full documentation covering
  request validation (body, headers, path, query), response validation
  (body, headers), combined req+res, and validation error formats (422/500)
- Add note that body validation is limited to JSON-serializable values due
  to how Standard Schema operates on JavaScript values
- Document ExtendedAPIGatewayProxyResult JSON body, array body, custom
  headers, and no-body (204) patterns in the auto-serialization section
- Add "Typing the response" subsection under HTTP Methods for compile-time
  only return type checking via app.get<User>() type arguments
- Add code snippets and sample JSON files for all new examples
…pescript into aidlc

# Conflicts:
#	package-lock.json
@sonarqubecloud
Copy link

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

Labels

dependencies Changes that touch dependencies, e.g. Dependabot, etc. documentation Improvements or additions to documentation event-handler This item relates to the Event Handler Utility size/XXL PRs with 1K+ LOC, largely documentation related tests PRs that add or change tests

Projects

Status: Working on it

Development

Successfully merging this pull request may close these issues.

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

3 participants