Saltar para o conteúdo
API Gateway + Lambda: Construindo REST API Serverless na AWS
AWS

API Gateway + Lambda: Construindo REST API Serverless na AWS

8 de agosto de 2024·Paulo de Paula

APIs serverless na AWS combinam API Gateway para roteamento HTTP com Lambda para execução de código. Você paga por requisição, escala automaticamente e não gerencia servidor. Para a maioria das APIs que não têm tráfego previsível e constante, o custo é menor que EC2.

HTTP API vs REST API Gateway

HTTP API:
  ✅ Custo ~70% menor
  ✅ Latência menor
  ✅ JWT authorizer nativo
  ❌ Sem API keys, uso plans, caching nativo

REST API:
  ✅ API keys e usage plans
  ✅ Caching de respostas
  ✅ Transformações de request/response (VTL)
  ❌ Custo maior

Para a maioria dos projetos: use HTTP API. Mude para REST API apenas se precisar de caching ou usage plans.

Estrutura do Lambda handler

// src/handler.js
export const handler = async (event, context) => {
  // event.httpMethod, event.path, event.headers disponíveis em REST API
  // event.requestContext.http.method para HTTP API

  const method = event.requestContext?.http?.method || event.httpMethod;
  const path   = event.requestContext?.http?.path   || event.path;

  console.log({ requestId: context.awsRequestId, method, path });

  try {
    const resultado = await processarRequisicao(event);
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': process.env.FRONTEND_URL || '*',
      },
      body: JSON.stringify(resultado),
    };
  } catch (error) {
    console.error('Erro não tratado:', error);
    return {
      statusCode: error.statusCode || 500,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        error: error.message || 'Erro interno',
        requestId: context.awsRequestId,
      }),
    };
  }
};

Roteamento dentro do Lambda

Com HTTP API você pode ter um Lambda por rota, ou um Lambda monolítico com roteamento interno. O segundo é mais simples para começar:

// src/router.js
import { listarProdutos, buscarProduto, criarProduto } from './handlers/produtos.js';
import { login, refresh } from './handlers/auth.js';

const routes = {
  'GET /produtos':      listarProdutos,
  'GET /produtos/{id}': buscarProduto,
  'POST /produtos':     criarProduto,
  'POST /auth/login':   login,
  'POST /auth/refresh': refresh,
};

export function rotear(event) {
  const method = event.requestContext.http.method;
  const path   = event.requestContext.http.path;
  
  // Normaliza path removendo stage (/prod/produtos → /produtos)
  const pathNormalizado = path.replace(/^\/[^/]+/, '');
  
  const chave = `${method} ${pathNormalizado}`;
  const handler = routes[chave];
  
  if (!handler) {
    return { statusCode: 404, body: JSON.stringify({ error: 'Rota não encontrada' }) };
  }
  
  return handler(event);
}

Parâmetros de path e query string

// event para GET /produtos/123?include=estoque
export async function buscarProduto(event) {
  const { id } = event.pathParameters;           // "123"
  const include = event.queryStringParameters?.include; // "estoque"
  
  const produto = await Produto.findById(id);
  if (!produto) {
    return { statusCode: 404, body: JSON.stringify({ error: 'Produto não encontrado' }) };
  }
  
  if (include === 'estoque') {
    produto.estoque = await Estoque.findByProduto(id);
  }
  
  return {
    statusCode: 200,
    body: JSON.stringify(produto),
  };
}

JWT Authorizer nativo (HTTP API)

// Lambda authorizer (mais controle)
export const authorizer = async (event) => {
  const token = event.headers?.authorization?.replace('Bearer ', '');
  
  if (!token) throw new Error('Unauthorized');
  
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    return {
      isAuthorized: true,
      context: {
        usuarioId: payload.sub,
        email:     payload.email,
        role:      payload.role,
      },
    };
  } catch {
    throw new Error('Unauthorized'); // HTTP API retorna 401
  }
};

No handler protegido, o contexto do authorizer fica disponível:

export async function criarProduto(event) {
  const usuarioId = event.requestContext.authorizer.lambda.usuarioId;
  const role      = event.requestContext.authorizer.lambda.role;
  
  if (role !== 'ADMIN') {
    return { statusCode: 403, body: JSON.stringify({ error: 'Sem permissão' }) };
  }
  // ...
}

Variáveis de ambiente com SSM

// Evita hardcoded secrets — busca do SSM Parameter Store
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';

const ssm = new SSMClient({ region: 'us-east-1' });

// Cache em memória (Lambda reutiliza o container)
const cache = new Map();

async function getParam(nome) {
  if (cache.has(nome)) return cache.get(nome);
  
  const { Parameter } = await ssm.send(new GetParameterCommand({
    Name: nome,
    WithDecryption: true,
  }));
  
  cache.set(nome, Parameter.Value);
  return Parameter.Value;
}

// Uso no handler
const dbUrl = await getParam('/minha-api/prod/DATABASE_URL');

Infraestrutura com CDK

// infra/api-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';

export class ApiStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const fn = new lambda.Function(this, 'ApiHandler', {
      runtime: lambda.Runtime.NODEJS_20_X,
      handler: 'src/handler.handler',
      code:    lambda.Code.fromAsset('dist'),
      timeout: cdk.Duration.seconds(30),
      memorySize: 512,
      environment: {
        NODE_ENV: 'production',
      },
    });

    const api = new apigwv2.HttpApi(this, 'HttpApi', {
      corsPreflight: {
        allowOrigins: ['https://meusite.com.br'],
        allowMethods: [apigwv2.CorsHttpMethod.ANY],
        allowHeaders: ['Authorization', 'Content-Type'],
      },
    });

    api.addRoutes({
      path:        '/{proxy+}',
      methods:     [apigwv2.HttpMethod.ANY],
      integration: new HttpLambdaIntegration('Proxy', fn),
    });

    new cdk.CfnOutput(this, 'ApiUrl', { value: api.apiEndpoint });
  }
}

Mitigando cold start

// Provisioned Concurrency: mantém N instâncias sempre quentes
const alias = new lambda.Alias(this, 'LiveAlias', {
  aliasName: 'live',
  version:   fn.currentVersion,
  provisionedConcurrentExecutions: 2, // 2 instâncias sempre aquecidas
});

Cold starts em Node.js com Lambda duram 100-500ms normalmente. Para reduzir:

  1. Use NodeNext target — imports lazy
  2. Evite imports de SDK completo (@aws-sdk/client-s3 em vez de aws-sdk)
  3. Inicialize conexões fora do handler (reutilizadas entre invocações)
  4. Provisioned Concurrency para rotas críticas

Custo vs EC2

API com 1 milhão de requisições/mês, 200ms média, 512MB:

Lambda:
  Requisições: 1M × $0.0000002  = $0.20
  Computação:  1M × 0.2s × $0.0000166667/GB-s × 0.5GB = $1.67
  Total: ~$2/mês

EC2 t3.micro (us-east-1):
  $0.0104/hora × 730h = $7.59/mês
  (sem contar ELB ~$16/mês + data transfer)

Serverless ganha para tráfego irregular. Com tráfego constante alto (>50 req/s), EC2 ou ECS costuma ser mais barato.