skills/aws-lambda-typescript-integration/references/fastify-adapter.md

7.3 KiB

Fastify Adapter for Lambda

Overview

Fastify offers superior performance compared to Express for Lambda workloads, with lower cold start times and better throughput.

Installation

npm install @nestjs/platform-fastify fastify aws-lambda-fastify
npm install -D @types/aws-lambda

Basic Configuration

Standard Setup

import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import awsLambdaFastify from 'aws-lambda-fastify';
import { AppModule } from './app.module';

let cachedProxy: any;

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter({
      logger: false,
      trustProxy: true,
      genReqId: () => `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
    }),
  );

  await app.init();

  return awsLambdaFastify(app.getHttpAdapter().getInstance(), {
    binaryMimeTypes: ['application/pdf', 'image/*'],
    serializeLambdaArguments: false,
  });
}

export const handler = async (event, context) => {
  if (!cachedProxy) {
    cachedProxy = await bootstrap();
  }
  return cachedProxy(event, context);
};

Fastify Options

Performance Tuning

const adapter = new FastifyAdapter({
  // Disable logging for cold start performance
  logger: false,

  // Trust proxy headers from API Gateway
  trustProxy: true,

  // Connection timeout (Lambda max is 30s)
  connectionTimeout: 29000,

  // Keep alive timeout
  keepAliveTimeout: 5000,

  // Max payload size (API Gateway limit is 10MB)
  bodyLimit: 10485760,

  // Case-sensitive routing
  caseSensitive: true,

  // Ignore trailing slashes
  ignoreTrailingSlash: true,

  // Max param length for URL parameters
  maxParamLength: 100,
});

Plugins

Compression

import compression from '@fastify/compress';

// Register compression plugin
app.register(compression, {
  global: true,
  encodings: ['gzip', 'deflate'],
  threshold: 1024, // Only compress responses > 1KB
});

CORS

import cors from '@fastify/cors';

app.register(cors, {
  origin: (origin, cb) => {
    const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
    if (!origin || allowedOrigins.includes(origin)) {
      cb(null, true);
      return;
    }
    cb(new Error('Not allowed'), false);
  },
  credentials: true,
  methods: ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
});

Helmet (Security Headers)

import helmet from '@fastify/helmet';

app.register(helmet, {
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
  },
});

Rate Limit

import rateLimit from '@fastify/rate-limit';

app.register(rateLimit, {
  max: 100,
  timeWindow: '15 minutes',
  keyGenerator: (req) => req.headers['x-forwarded-for'] || req.ip,
  errorResponseBuilder: (req, context) => ({
    statusCode: 429,
    error: 'Too Many Requests',
    message: `Rate limit exceeded. Try again in ${context.after}`,
    retryAfter: context.after,
  }),
});

Request/Response Hooks

Lifecycle Hooks

// On request hook
app.getHttpAdapter().getInstance().addHook('onRequest', async (request, reply) => {
  // Add request ID
  reply.header('x-request-id', request.id);

  // Log request
  console.log({
    event: 'request_start',
    method: request.method,
    url: request.url,
    requestId: request.id,
    timestamp: new Date().toISOString(),
  });
});

// On send hook
app.getHttpAdapter().getInstance().addHook('onSend', async (request, reply, payload) => {
  // Log response
  console.log({
    event: 'request_end',
    method: request.method,
    url: request.url,
    statusCode: reply.statusCode,
    duration: Date.now() - (request as any).startTime,
  });
});

Validation

JSON Schema Validation

// Fastify uses JSON Schema for validation
const createUserSchema = {
  schema: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string', minLength: 1, maxLength: 100 },
        email: { type: 'string', format: 'email' },
        age: { type: 'integer', minimum: 0, maximum: 150 },
      },
    },
    response: {
      201: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          name: { type: 'string' },
          email: { type: 'string' },
        },
      },
    },
  },
};

// Use in controller
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createUserDto: CreateUserDto) {
  return this.usersService.create(createUserDto);
}

Serialization

Custom Serializer

// Fastify-serialize options
app.register(require('@fastify/response-validation'), {
  onError: (error) => {
    console.error('Response validation error:', error);
  },
});

// Use class-transformer for serialization
@Controller('users')
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
  // Responses automatically serialized
}

Error Handling

Global Error Handler

// Set error handler on Fastify instance
app.getHttpAdapter().getInstance().setErrorHandler((error, request, reply) => {
  console.error({
    error: error.message,
    stack: error.stack,
    code: error.code,
    validation: error.validation,
  });

  // Handle validation errors
  if (error.validation) {
    reply.status(400).send({
      statusCode: 400,
      error: 'Bad Request',
      message: error.message,
      validation: error.validation,
    });
    return;
  }

  // Handle other errors
  const statusCode = error.statusCode || 500;
  reply.status(statusCode).send({
    statusCode,
    error: error.name || 'Internal Server Error',
    message: process.env.NODE_ENV === 'production'
      ? 'Internal server error'
      : error.message,
  });
});

File Upload

Multipart Support

import multipart from '@fastify/multipart';

app.register(multipart, {
  limits: {
    fieldNameSize: 100,
    fieldSize: 1000000, // 1MB
    fields: 10,
    fileSize: 10000000, // 10MB
    files: 5,
  },
});

// Controller handling
@Post('upload')
async upload(@Req() req: FastifyRequest) {
  const data = await req.file();

  // Process file
  const buffer = await data.toBuffer();

  return {
    filename: data.filename,
    mimetype: data.mimetype,
    size: buffer.length,
  };
}

Performance Comparison

Benchmarks

Metric Express Fastify Improvement
Cold Start ~250ms ~180ms 28% faster
Throughput 15k req/s 25k req/s 67% higher
Memory Usage 85MB 65MB 24% less
JSON Parsing 12k ops/s 25k ops/s 108% faster

When to Choose Fastify

  • High throughput APIs - Many concurrent requests
  • JSON-heavy APIs - Superior JSON parsing performance
  • Memory-constrained environments - Lower memory footprint
  • Cold start sensitive - Faster initialization

When to Choose Express

  • Existing Express middleware - Large ecosystem compatibility
  • Migration projects - Easier migration path
  • Complex routing - More mature routing patterns
  • Team familiarity - If team knows Express well