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
444 changes: 270 additions & 174 deletions docs/features/event-handler/rest.md

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions examples/snippets/event-handler/rest/advanced_binary_responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { readFile } from 'node:fs/promises';
import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest';
import { compress } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware';
import type { Context } from 'aws-lambda';

const app = new Router();

app.get('/logo', [compress()], async () => {
const logoFile = await readFile(`${process.env.LAMBDA_TASK_ROOT}/logo.png`);
return {
body: logoFile.toString('base64'),
isBase64Encoded: true,
headers: {
'Content-Type': 'image/png',
},
statusCode: 200,
};
});

export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
17 changes: 17 additions & 0 deletions examples/snippets/event-handler/rest/advanced_compress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
declare function getTodoById<T>(todoId: unknown): Promise<{ id: string } & T>;

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

const app = new Router();

app.use(compress());

app.get('/todos/:todoId', async ({ todoId }) => {
const todo = await getTodoById(todoId);
return { todo };
});

export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
26 changes: 26 additions & 0 deletions examples/snippets/event-handler/rest/advanced_cors_per_route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
declare function getTodoById<T>(todoId: unknown): Promise<{ id: string } & T>;

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

const app = new Router();

app.use(
cors({
origin: 'https://example.com',
maxAge: 300,
})
);

app.get('/todos/:todoId', async ({ todoId }) => {
const todo = await getTodoById(todoId);
return { todo };
});

app.get('/health', [cors({ origin: '*' })], async () => {
return { status: 'ok' };
});

export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
22 changes: 22 additions & 0 deletions examples/snippets/event-handler/rest/advanced_cors_simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
declare function getTodoById<T>(todoId: unknown): Promise<{ id: string } & T>;

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

const app = new Router();

app.use(
cors({
origin: 'https://example.com',
maxAge: 300,
})
);

app.get('/todos/:todoId', async ({ todoId }) => {
const todo = await getTodoById(todoId);
return { todo };
});

export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ app.get('/todos', async () => {
});
});

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

Expand All @@ -36,6 +36,5 @@ app.post('/todos', async (params, reqCtx) => {
});
});

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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
declare const getAllTodos: () => Promise<Record<string, string>[]>;
declare const putTodo: (body: unknown) => Promise<Record<string, string>>;

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

const app = new Router();

app.use(apiMiddleware);

app.get('/todos', async () => {
const todos = await getAllTodos();
return { todos };
});

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

export const handler = async (event: unknown, context: Context) =>
app.resolve(event, context);
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { composeMiddleware } from '@aws-lambda-powertools/event-handler/experimental-rest';
import { cors } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware';
import type { Middleware } from '@aws-lambda-powertools/event-handler/types';
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}`);
await next();
logger.info(`Response: ${reqCtx.res.status}`);
};

const rateLimit: Middleware = async (_, reqCtx, next) => {
// Rate limiting logic would go here
reqCtx.res.headers.set('X-RateLimit-Limit', '100');
await next();
};

// Reusable composed middleware
const apiMiddleware = composeMiddleware([logging, cors(), rateLimit]);

export { apiMiddleware };
Original file line number Diff line number Diff line change
@@ -1,48 +1,57 @@
declare const compresssBody: (body: string) => Promise<string>;
declare const getUserTodos: (
userId: string
) => Promise<Record<string, string>[]>;
declare const jwt: {
verify(token: string, secret: string): { sub: string; roles: string[] };
};

import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest';
import { getStringFromEnv } from '@aws-lambda-powertools/commons/utils/env';
import {
Router,
UnauthorizedError,
} from '@aws-lambda-powertools/event-handler/experimental-rest';
import type { Middleware } from '@aws-lambda-powertools/event-handler/types';
import { Logger } from '@aws-lambda-powertools/logger';
import type { Context } from 'aws-lambda';

interface CompressOptions {
threshold?: number;
level?: number;
}

// Factory function that returns middleware
const compress = (options: CompressOptions = {}): Middleware => {
return async (params, reqCtx, next) => {
await next();
const jwtSecret = getStringFromEnv({
key: 'JWT_SECRET',
errorMessage: 'JWT_SECRET is not set',
});

// Check if response should be compressed
const body = await reqCtx.res.text();
const threshold = options.threshold || 1024;
const logger = new Logger({});
const app = new Router();
const store: { userId: string; roles: string[] } = { userId: '', roles: [] };

if (body.length > threshold) {
const compressedBody = await compresssBody(body);
const compressedRes = new Response(compressedBody, reqCtx.res);
compressedRes.headers.set('Content-Encoding', 'gzip');
reqCtx.res = compressedRes;
// Factory function that returns middleware
const verifyToken = (options: { jwtSecret: string }): Middleware => {
return async (_, { request }, next) => {
const auth = request.headers.get('Authorization');
if (!auth || !auth.startsWith('Bearer '))
return new UnauthorizedError('Missing or invalid Authorization header');

const token = auth.slice(7);
try {
const payload = jwt.verify(token, options.jwtSecret);
store.userId = payload.sub;
store.roles = payload.roles;
} catch (error) {
logger.error('Token verification failed', { error });
return new UnauthorizedError('Invalid token');
}

await next();
};
};

const app = new Router();

// Use custom middleware globally
app.use(compress({ threshold: 500 }));
app.use(verifyToken({ jwtSecret }));

app.get('/data', async () => {
return {
message: 'Large response data',
data: new Array(100).fill('content'),
};
app.post('/todos', async (_) => {
const { userId } = store;
const todos = await getUserTodos(userId);
return { todos };
});

app.get('/small', async () => {
return { message: 'Small response' };
});

export const handler = async (event: unknown, context: Context) => {
return await 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 @@ -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 (params, { res }, next) => {
const _badMiddleware: Middleware = async (_, { 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 (params, 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 All @@ -26,6 +26,5 @@ app.get('/test', async () => {
return { message: 'Hello World!' };
});

export const handler = async (event: unknown, context: Context) => {
return await 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 @@ -9,7 +9,7 @@ const logger = new Logger();
const app = new Router({ logger });

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

if (!authHeader) {
Expand All @@ -23,7 +23,7 @@ const authMiddleware: Middleware = async (params, reqCtx, next) => {
};

// Logging middleware - never executes when auth fails
const loggingMiddleware: Middleware = async (params, reqCtx, next) => {
const loggingMiddleware: Middleware = async (_, __, next) => {
logger.info('Request processed');
await next();
};
Expand All @@ -36,6 +36,5 @@ app.get('/todos', async () => {
return { todos };
});

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 @@ -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 (params, 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 (params, 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,7 +31,7 @@ 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 (params, reqCtx) => {
app.post('/todos', [routeMiddleware], async (_, reqCtx) => {
const body = await reqCtx.request.json();
const todo = await putTodo(body);
return todo;
Expand Down
Loading