API Gateway + Lambda: Construindo REST API Serverless na AWS
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 maiorPara 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:
- Use
NodeNexttarget — imports lazy - Evite imports de SDK completo (
@aws-sdk/client-s3em vez deaws-sdk) - Inicialize conexões fora do handler (reutilizadas entre invocações)
- 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.