Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions docs/features/event-handler/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Please [check this issue](https://github.com/aws-powertools/powertools-lambda-ty

### Accessing request details

You can access request details such as headers, query parameters, and body using the `Request` object provided to your route handlers and middleware functions via `reqCtx.request`.
You can access request details such as headers, query parameters, and body using the `Request` object provided to your route handlers and middleware functions via `reqCtx.req`.

### Handling not found routes

Expand All @@ -148,6 +148,8 @@ You can use the `errorHandler()` method as a higher-order function or class meth

This allows you to catch and return custom error responses, or perform any other error handling logic you need.

Error handlers receive the error object and the request context as arguments, and can return a [`Response` object](#returning-response-objects) or a JavaScript object that will be auto-serialized as per the [response auto-serialization](#response-auto-serialization) section.

!!! tip "You can also pass a list of error classes to the `errorHandler()` method."

=== "index.ts"
Expand All @@ -158,15 +160,17 @@ This allows you to catch and return custom error responses, or perform any other

### Throwing HTTP errors

You can throw HTTP errors in your route handlers to return specific HTTP status codes and messages. Event Handler provides a set of built-in HTTP error classes that you can use to throw common HTTP errors.
You can throw HTTP errors in your route handlers to stop execution and return specific HTTP status codes and messages. Event Handler provides a set of built-in HTTP error classes that you can use to throw common HTTP errors.

This ensures that your Lambda function doesn't fail but returns a well-defined HTTP error response to the client.

If you need to send custom headers or a different response structure/code, you can use the [Response](#returning-response-objects) object instead.

!!! tip "You can throw HTTP errors in your route handlers, middleware, or custom error handlers!"

=== "index.ts"

```ts hl_lines="3 10"
```ts hl_lines="3 11"
--8<-- "examples/snippets/event-handler/rest/gettingStarted_throwing_http_errors.ts:3"
```

Expand Down Expand Up @@ -194,15 +198,21 @@ All error classes accept optional parameters for custom messages and additional

### Route prefixes

!!! note "Coming soon"

When defining multiple routes related to a specific resource, it's common to have a shared prefix. For example, you might have several routes that all start with `/todos`.

For example, if you have a custom domain `api.example.com` and you want to map it to the `/v1` base path of your API. In this case, all the requests will contain `/v1/<resource>` in the path, requiring you to repeat the `/v1` prefix in all your route definitions.

At the moment, you have to manually include the prefix in each route definition, however we are planning to add support for route prefixes in a future release.
To avoid repeating the prefix in each route definition, you can use the `prefix` constructor parameter when creating a new `Router` instance, and we'll automatically strip it from the request path before matching routes. After mapping a path prefix, the new root path will automatically be mapped to the path argument of `/`.

=== "index.ts"

```ts hl_lines="4 7"
--8<-- "examples/snippets/event-handler/rest/gettingStarted_route_prefix.ts:3"
```

Please [check this issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/4513) for more details and examples, and add 👍 if you would like us to prioritize it.
This is also useful when splitting routes into separate files (see [Split routers](#split-routers) section) or when using [API mappings](https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-mappings.html){target="_blank"} to map custom domains to specific base paths.

For example, when using `prefix: '/pay'`, there is no difference between a request path of `/pay` and `/pay/`; and the path argument would be defined as `/`.

## Advanced

Expand All @@ -213,9 +223,8 @@ incoming request and your route handler. They provide a way to implement cross-c
concerns like authentication, logging, validation, and response transformation without
cluttering your route handlers.

Each middleware function receives the following arguments:
Each middleware function receives two arguments:

* **params** - Route parameters extracted from the URL path
* **reqCtx** - Request context containing the event, Lambda context, request, and response objects
* **next** - A function to pass control to the next middleware in the chain

Expand Down Expand Up @@ -312,6 +321,11 @@ that no post-processing of your request will occur.
A common pattern to create reusable middleware is to implement a factory functions that
accepts configuration options and returns a middleware function.

!!! note "Always `await next()` unless returning early"
Middleware functions must always call `await next()` to pass control to the next middleware
in the chain, unless you are intentionally returning early by returning a `Response` or
JSON object.

=== "index.ts"

```ts hl_lines="20-21 36 41"
Expand Down
2 changes: 1 addition & 1 deletion examples/snippets/event-handler/rest/advanced_compress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const app = new Router();

app.use(compress());

app.get('/todos/:todoId', async ({ todoId }) => {
app.get('/todos/:todoId', async ({ params: { todoId } }) => {
const todo = await getTodoById(todoId);
return { todo };
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ app.use(
})
);

app.get('/todos/:todoId', async ({ todoId }) => {
app.get('/todos/:todoId', async ({ params: { todoId } }) => {
const todo = await getTodoById(todoId);
return { todo };
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ app.use(
})
);

app.get('/todos/:todoId', async ({ todoId }) => {
app.get('/todos/:todoId', async ({ params: { todoId } }) => {
const todo = await getTodoById(todoId);
return { todo };
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ app.get('/todos', async () => {
});
});

app.post('/todos', async (_, reqCtx) => {
const body = await reqCtx.request.json();
app.post('/todos', async ({ req }) => {
const body = await req.json();
const todo = await createTodo(body.title);

return new Response(JSON.stringify(todo), {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ app.get('/todos', async () => {
return { todos };
});

app.post('/todos', async (_, { request }) => {
const body = await request.json();
app.post('/todos', async ({ req }) => {
const body = await req.json();
const todo = await putTodo(body);
return todo;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { Logger } from '@aws-lambda-powertools/logger';

const logger = new Logger();

const logging: Middleware = async (_, reqCtx, next) => {
logger.info(`Request: ${reqCtx.request.method} ${reqCtx.request.url}`);
const logging: Middleware = async ({ reqCtx, next }) => {
logger.info(`Request: ${reqCtx.req.method} ${reqCtx.req.url}`);
await next();
logger.info(`Response: ${reqCtx.res.status}`);
};

const rateLimit: Middleware = async (_, reqCtx, next) => {
const rateLimit: Middleware = async ({ reqCtx, next }) => {
// Rate limiting logic would go here
reqCtx.res.headers.set('X-RateLimit-Limit', '100');
await next();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ const store: { userId: string; roles: string[] } = { userId: '', roles: [] };

// Factory function that returns middleware
const verifyToken = (options: { jwtSecret: string }): Middleware => {
return async (_, { request }, next) => {
const auth = request.headers.get('Authorization');
return async ({ reqCtx: { req }, next }) => {
const auth = req.headers.get('Authorization');
if (!auth || !auth.startsWith('Bearer '))
return new UnauthorizedError('Missing or invalid Authorization header');
throw new UnauthorizedError('Missing or invalid Authorization header');

const token = auth.slice(7);
try {
Expand All @@ -37,7 +37,7 @@ const verifyToken = (options: { jwtSecret: string }): Middleware => {
store.roles = payload.roles;
} catch (error) {
logger.error('Token verification failed', { error });
return new UnauthorizedError('Invalid token');
throw new UnauthorizedError('Invalid token');
}

await next();
Expand All @@ -47,7 +47,7 @@ const verifyToken = (options: { jwtSecret: string }): Middleware => {
// Use custom middleware globally
app.use(verifyToken({ jwtSecret }));

app.post('/todos', async (_) => {
app.post('/todos', async () => {
const { userId } = store;
const todos = await getUserTodos(userId);
return { todos };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import type { Context } from 'aws-lambda';
const app = new Router();

// ❌ WRONG: Using destructuring captures a reference to the original response
const _badMiddleware: Middleware = async (_, { res }, next) => {
const _badMiddleware: Middleware = async ({ reqCtx: { res }, next }) => {
res.headers.set('X-Before', 'Before');
await next();
// This header will NOT be added because 'res' is a stale reference
res.headers.set('X-After', 'After');
};

// ✅ CORRECT: Always access response through reqCtx
const goodMiddleware: Middleware = async (_, reqCtx, next) => {
const goodMiddleware: Middleware = async ({ reqCtx, next }) => {
reqCtx.res.headers.set('X-Before', 'Before');
await next();
// This header WILL be added because we get the current response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const logger = new Logger();
const app = new Router({ logger });

// Authentication middleware - returns early if no auth header
const authMiddleware: Middleware = async (_, reqCtx, next) => {
const authHeader = reqCtx.request.headers.get('authorization');
const authMiddleware: Middleware = async ({ reqCtx, next }) => {
const authHeader = reqCtx.req.headers.get('authorization');

if (!authHeader) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
Expand All @@ -23,7 +23,7 @@ const authMiddleware: Middleware = async (_, reqCtx, next) => {
};

// Logging middleware - never executes when auth fails
const loggingMiddleware: Middleware = async (_, __, next) => {
const loggingMiddleware: Middleware = async ({ next }) => {
logger.info('Request processed');
await next();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ const logger = new Logger();
const app = new Router({ logger });

// Global middleware - executes first in pre-processing, last in post-processing
app.use(async (_, reqCtx, next) => {
app.use(async ({ reqCtx, next }) => {
reqCtx.res.headers.set('x-pre-processed-by', 'global-middleware');
await next();
reqCtx.res.headers.set('x-post-processed-by', 'global-middleware');
});

// Route-specific middleware - executes second in pre-processing, first in post-processing
const routeMiddleware: Middleware = async (_, reqCtx, next) => {
const routeMiddleware: Middleware = async ({ reqCtx, next }) => {
reqCtx.res.headers.set('x-pre-processed-by', 'route-middleware');
await next();
reqCtx.res.headers.set('x-post-processed-by', 'route-middleware');
Expand All @@ -31,12 +31,11 @@ app.get('/todos', async () => {
// This route will have:
// x-pre-processed-by: route-middleware (route middleware overwrites global)
// x-post-processed-by: global-middleware (global middleware executes last)
app.post('/todos', [routeMiddleware], async (_, reqCtx) => {
const body = await reqCtx.request.json();
app.post('/todos', [routeMiddleware], async ({ req }) => {
const body = await req.json();
const todo = await putTodo(body);
return todo;
});

export const handler = async (event: unknown, context: Context) => {
return app.resolve(event, context);
};
export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ sequenceDiagram
participant Handler as Route Handler

Request->>Router: Incoming Request
Router->>M1: Execute (params, reqCtx, next)
Router->>M1: Execute ({ reqCtx, next })
Note over M1: Pre-processing
M1->>M2: Call next()
M1->>M2: Call await next()
Note over M2: Pre-processing
M2->>M2: Return Response (early return)
Note over M2: Post-processing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ sequenceDiagram
participant Handler as Route Handler

Request->>Router: Incoming Request
Router->>M1: Execute (params, reqCtx, next)
Router->>M1: Execute ({ reqCtx, next })
Note over M1: Pre-processing
M1->>M2: Call next()
M1->>M2: Call await next()
Note over M2: Throws Error
M2-->>M1: Error propagated
M1-->>Router: Error propagated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ sequenceDiagram
participant Handler as Route Handler

Request->>Router: Incoming Request
Router->>GM: Execute (params, reqCtx, next)
Router->>GM: Execute ({ reqCtx, next })
Note over GM: Pre-processing
GM->>RM: Call next()
GM->>RM: Call await next()
Note over RM: Pre-processing
RM->>Handler: Call next()
RM->>Handler: Call await next()
Note over Handler: Execute handler
Handler-->>RM: Return
Note over RM: Post-processing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ sequenceDiagram
participant Handler as Route Handler

Request->>Router: Incoming Request
Router->>M1: Execute (params, reqCtx, next)
Router->>M1: Execute ({ reqCtx, next })
Note over M1: Pre-processing
M1->>M2: Call next()
M1->>M2: Call await next()
Note over M2: Error thrown & caught
Note over M2: Handle error gracefully
M2->>Handler: Call next()
M2->>Handler: Call await next()
Note over Handler: Execute handler
Handler-->>M2: Return
Note over M2: Post-processing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ sequenceDiagram
participant Handler as Route Handler

Request->>Router: Incoming Request
Router->>M1: Execute (params, reqCtx, next)
Router->>M1: Execute ({ reqCtx, next })
Note over M1: Pre-processing
M1->>M2: Call next()
M1->>M2: Call await next()
Note over M2: Intentionally throws error
M2-->>M1: Error propagated
M1-->>Router: Error propagated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const logger = new Logger({
});
const app = new Router({ logger });

app.get('/todos/:todoId', async ({ todoId }) => {
app.get('/todos/:todoId', async ({ params: { todoId } }) => {
const todo = await getTodoById(todoId);
return { todo };
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@ app.errorHandler(GetTodoError, async (error, reqCtx) => {

return {
statusCode: HttpStatusCodes.BAD_REQUEST,
message: `Bad request: ${error.message} - ${reqCtx.request.headers.get('x-correlation-id')}`,
error: 'BadRequest',
message: `Bad request: ${error.message} - ${reqCtx.req.headers.get('x-correlation-id')}`,
};
});

app.get('/todos/:todoId', async ({ todoId }) => {
app.get('/todos/:todoId', async ({ params: { todoId } }) => {
const todo = await getTodoById(todoId); // May throw GetTodoError
return { todo };
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ app.notFound(async (error, reqCtx) => {
statusCode: HttpStatusCodes.IM_A_TEAPOT,
body: "I'm a teapot!",
headers: {
'x-correlation-id': reqCtx.request.headers.get('x-correlation-id'),
'x-correlation-id': reqCtx.req.headers.get('x-correlation-id'),
},
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const logger = new Logger({
});
const app = new Router({ logger });

app.post('/todos', async (_, { request }) => {
const body = await request.json();
app.post('/todos', async ({ req }) => {
const body = await req.json();
const todo = await putTodo(body);

return todo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const logger = new Logger({
const app = new Router({ logger });

app.route(
async (_, { request }) => {
const body = await request.json();
async ({ req }) => {
const body = await req.json();
const todo = await putTodo(body);

return todo;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
declare function getUserTodos(auth: string | null): Promise<{ id: string }>;

import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest';
import type { Context } from 'aws-lambda';

const app = new Router({ prefix: '/todos' });

// matches POST /todos
app.post('/', async ({ req: { headers } }) => {
const todos = await getUserTodos(headers.get('Authorization'));
return { todos };
});

export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
Loading