Teu progresso
0 / 83 módulos0%
Estágio 02 · 02-08
Bloqueadohttp puro de Node é spartano. Você manualmente parseia URL, body, multipart, escreve roteamento, lida com errors. Em produção real, frameworks tomam conta dessa fundação. Mas há diferença real entre eles: throughput, ergonomia, modelo de plugins, type safety, edge-readiness, ecosystem. Escolher errado é dor cara, refactor de framework em projeto vivo é trabalho de meses.
Este módulo dissecca os principais. Não é "qual é o melhor". É: como cada um foi desenhado, que problema ele optou por resolver, que problema ele empurrou pra você, e como escolher conforme o contexto.
Em essência, todos resolvem:
Diferenças nascem em: ergonomia, performance, schema validation, tipagem fim-a-fim, runtime targets (Node, Edge, Bun, Deno).
Lançado em 2010. Mais antigo, mais usado, mais ecosystem. Modelo:
import express from 'express';
const app = express();
app.use(express.json());
app.get('/orders/:id', async (req, res) => {
const order = await findById(req.params.id);
res.json(order);
});
app.listen(3000);
Pontos:
app.use, (req, res, next) => {}).next(err). Express 5 (estável agora) trata async automaticamente.passport, helmet, cors, morgan, etc.Quando vence: time já sabe; projeto pequeno-médio; cargas baixas-médias; integrar com plugin específico que só Express tem.
Lançado em 2017, foco em performance e schema validation:
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
fastify.get('/orders/:id', {
schema: {
params: { type: 'object', properties: { id: { type: 'string' } } },
response: {
200: { type: 'object', properties: { id: { type: 'string' }, total: { type: 'number' } } }
}
}
}, async (req) => findById(req.params.id));
await fastify.listen({ port: 3000 });
Pontos:
JSON.stringify).onRequest, preParsing, preValidation, preHandler, onSend, onResponse, onError.pino) por default.Quando vence: você quer performance e estrutura disciplinada; precisa validar schemas; vai escalar pra time grande.
Lançado em 2022. Framework moderno desenhado pra rodar em qualquer runtime: Node, Bun, Deno, Cloudflare Workers, Vercel Edge, AWS Lambda. Web Standard APIs (Request, Response, fetch).
import { Hono } from 'hono';
const app = new Hono();
app.get('/orders/:id', async (c) => {
const id = c.req.param('id');
const order = await findById(id);
return c.json(order);
});
export default app; // funciona em Workers, Bun, Deno
// adapter pra Node:
import { serve } from '@hono/node-server';
serve(app);
Pontos:
Request/Response nativos.@hono/zod-validator pra schema validation.@hono/zod-openapi pra OpenAPI auto.Quando vence: edge/multi-runtime, microservices serverless, projetos modernos onde você quer não estar preso a Node.
Lançado em 2017, inspirado em Angular. Framework opinado, full-featured.
@Controller('orders')
export class OrdersController {
constructor(private readonly service: OrdersService) {}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.service.findOne(id);
}
}
Pontos:
Quando vence: empresa grande, time grande, time vindo de Java/.NET/C#, app complexo (microservices, GraphQL, WebSocket gateways) onde estrutura forte vale mais que leveza.
Lançado em 2023, primeiro feito pra Bun. Foca em DX e tipagem fim-a-fim:
import { Elysia, t } from 'elysia';
new Elysia()
.get('/orders/:id', ({ params }) => findById(params.id), {
params: t.Object({ id: t.String() })
})
.listen(3000);
Pontos:
Quando vence: projetos novos em Bun com foco em DX e tipagem.
Não são "frameworks HTTP" tradicionais, são camadas:
tRPC: RPC type-safe entre TS client e TS server. Você define procedures no server, client tem tipos automáticos. Roda sobre Express, Fastify, Next, Hono.
GraphQL: schema separado, query language. Apollo, Yoga, Mercurius (Fastify-based).
Decisão entre REST, tRPC, GraphQL:
Pipeline. Cada middleware recebe (req, res, next) (Express) ou retorna funções com next (Koa/Hono). Pode:
Comum a todos:
Padrão emergente: definir schema (zod/valibot/TypeBox), validar em entrada, derivar TS types e OpenAPI. Single source of truth.
OpenAPI/Swagger é a spec pra documentar APIs REST. Geração automática a partir do código:
@fastify/swagger lê schemas e gera spec.@hono/zod-openapi.@nestjs/swagger baseado em decorators e DTOs.swagger-jsdoc ou tsoa (TS Annotation, controller-first).Sem OpenAPI, integração externa vira documentação manual desatualizada. Sempre gere.
Padrão: structured logs (JSON) com:
Logs vão pra stdout. Coletor (Loki, CloudWatch, Datadog) recolhe.
Erros devem virar respostas HTTP previsíveis:
Erros específicos do domínio: subclasses de Error com code, status, details. Middleware central traduz pra response. Evite expor stacks em prod.
Problem Details (RFC 9457) é formato padronizado pra error responses. Considere.
Lib: express-rate-limit, @fastify/rate-limit, hono-rate-limiter, ou Redis-based pra distribuído.
Algoritmos:
Em backend distribuído (múltiplas instâncias), você precisa store compartilhado (Redis). Em single instance, in-memory basta. CDN/proxy pode aplicar antes de chegar (Cloudflare, AWS WAF).
Content-Encoding: gzip ou br (Brotli). Lib: compression (Express), @fastify/compress, etc.
Cache headers:
Cache-Control: public, max-age=3600ETag + If-None-Match → 304Last-Modified + If-Modified-Since → 304Vary pra negociação (Accept-Language, Accept-Encoding).Em backend de API frequentemente você faz Cache-Control: private, no-store ou tags específicas. CDN ainda pode cachear se você marca explicitamente.
Cross-Origin Resource Sharing. Browser bloqueia por default; server precisa explicitar Access-Control-Allow-Origin, -Methods, -Headers, etc.
Cuidado com:
Access-Control-Allow-Origin: * + credentials → bloqueado pelo browser. Pra credentials você lista origin explícita.OPTIONS) pra requests não-simple.Logística é multi-tenant (lojistas, entregadores, clientes). Padrões:
acme.api.example.com → middleware identifica tenant.X-Tenant-Id: acme./t/acme/orders.Onde tenant fica no contexto: AsyncLocalStorage, ou objeto de request anotado.
Nginx, Caddy, Traefik, Cloudflare na frente. Backend recebe X-Forwarded-For, X-Forwarded-Proto, etc. Frameworks têm trust proxy setting, habilite quando atrás de proxy.
req.ip real só é confiável se trust proxy estiver configurado e proxy injetar headers.
Cenário backend Node/TS 2026 mudou: Express maduro mas slow + sem types nativos; Fastify domina prod tradicional; Hono explode em edge runtimes; Elysia (Bun-native) pivot pra TypeScript-first; Next.js Route Handlers cobrem casos full-stack. Esta seção entrega benchmarks reais (sem hype), decision tree por workload, e migration paths concretos.
Performance benchmarks 2026 (req/s em "hello world", Node 22 single core, M2 Pro):
| Framework | RPS | Latency p99 | Notes |
|---|---|---|---|
| Bun + Bun.serve | ~250k | 0.4ms | Bun runtime nativo |
| Hono + Bun | ~240k | 0.5ms | Same runtime, slim wrapper |
| Elysia + Bun | ~220k | 0.6ms | TypeScript-first DX |
| Fastify + Node | ~85k | 1.8ms | Best Node-native |
| Hono + Node | ~80k | 1.9ms | Universal runtime |
| Hono + Cloudflare Workers | edge-distributed | 2-15ms (por região) | Não comparable diretamente |
| Express + Node | ~25k | 4.5ms | Maduro, slow |
| Next.js Route Handler | ~20k | 6ms | Bundled com Next; overhead RSC pipeline |
Disclaimer: hello-world não reflete prod. Fastify ≈ Hono em prod real (db queries dominate). Use perf como tiebreaker, não driver primário.
Fastify — opção default 2026 pra Node tradicional:
import Fastify from 'fastify';
import { z } from 'zod';
import { fastifyZod } from 'fastify-type-provider-zod';
const app = Fastify({ logger: true });
app.setValidatorCompiler(fastifyZod.validatorCompiler);
app.setSerializerCompiler(fastifyZod.serializerCompiler);
const orderSchema = z.object({
items: z.array(z.object({ productId: z.string().uuid(), qty: z.number().int().positive() })),
courierId: z.string().uuid().optional(),
});
app.post('/orders', {
schema: { body: orderSchema, response: { 200: z.object({ id: z.string().uuid() }) } },
}, async (req) => {
const order = await db.orders.insert({ ...req.body, tenantId: req.tenantId });
return { id: order.id };
});
await app.listen({ port: 8080, host: '0.0.0.0' });
Hono — universal runtime winner:
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono();
app.post('/orders',
zValidator('json', z.object({ items: z.array(z.object({ productId: z.string().uuid(), qty: z.number().int().positive() })), courierId: z.string().uuid().optional() })),
async (c) => {
const body = c.req.valid('json');
const order = await db.orders.insert({ ...body, tenantId: c.var.tenantId });
return c.json({ id: order.id });
}
);
export default app; // funciona em Cloudflare Workers, Bun, Deno, Node, AWS Lambda
Elysia — TypeScript-first em Bun:
import { Elysia, t } from 'elysia';
new Elysia()
.post('/orders', async ({ body, set }) => {
const order = await db.orders.insert(body);
return { id: order.id };
}, {
body: t.Object({
items: t.Array(t.Object({ productId: t.String({ format: 'uuid' }), qty: t.Integer({ minimum: 1 }) })),
courierId: t.Optional(t.String({ format: 'uuid' })),
}),
})
.listen(8080);
Next.js Route Handlers (full-stack):
// app/api/orders/route.ts
import { NextResponse } from 'next/server';
import { z } from 'zod';
const schema = z.object({
items: z.array(z.object({ productId: z.string().uuid(), qty: z.number().int().positive() })),
courierId: z.string().uuid().optional(),
});
export async function POST(req: Request) {
const body = await req.json();
const parsed = schema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: parsed.error }, { status: 400 });
const order = await db.orders.insert(parsed.data);
return NextResponse.json({ id: order.id });
}
Decision tree por workload:
| Workload | Framework |
|---|---|
| API CRUD em Node K8s, equipe Node experiente | Fastify + Zod |
| Edge deploy Cloudflare Workers / Vercel Edge | Hono |
| Microservices small com possível port pra Workers depois | Hono em Node primeiro |
| Greenfield Bun + TypeScript-first DX | Elysia |
| App fullstack React + simples API endpoints | Next.js Route Handlers |
| Migration de Express com low risk | Fastify (similar API) |
| Realtime WebSockets heavy | Fastify + websocket plugin OR Hono + Bun.serve |
| gRPC | Connect-RPC (separate) — fora desta lista |
Migration patterns:
Express → Fastify:
@fastify/express permite middleware Express usar Fastify gradualmente.app.use(...) por app.register(...) com hook lifecycle.Express → Hono:
Node → Bun:
Plugins ecosystem comparison (essential libs):
| Need | Fastify | Hono | Elysia |
|---|---|---|---|
| Auth JWT | @fastify/jwt | hono/jwt | @elysiajs/jwt |
| CORS | @fastify/cors | hono/cors | @elysiajs/cors |
| Rate limit | @fastify/rate-limit | hono-rate-limiter | @elysiajs/rate-limit |
| OpenAPI | @fastify/swagger | hono-openapi | @elysiajs/swagger |
| WebSocket | @fastify/websocket | hono/ws | built-in |
| Static files | @fastify/static | hono/serve-static | @elysiajs/static |
| Multipart | @fastify/multipart | hono/body | built-in |
Anti-patterns observados:
Cruza com 02-08 §2.10 (OpenAPI auto-gen é feature comum), 02-08 §2.17 (reverse proxy serve qualquer framework), 02-07 §2.17 (Node vs Bun vs Deno runtime decision), 02-05 §2.21 (Next.js cache layers se for Route Handlers), 04-08 §2.21 (Saga orchestration via Temporal — agnostic ao framework HTTP).
Por que plugin systems importam: código modular exige encapsulation, reuse entre serviços e boundaries claros. Sem plugin pattern, surge "god app.ts" com 500 routes + middleware globais misturados — mudança em auth quebra orders, swap Postgres → SQLite (testes) requer rewrite. Plugin system entrega: swap implementations por env, conditional features (admin-only routes), multi-tenant module boundaries.
Fastify encapsulation model (Fastify 5+): cada plugin roda em own context — hooks, decorators, schemas registrados não vazam pro parent scope. fastify-plugin 5+ (fp(fn, opts)) wrappa o plugin pulando encapsulation; decorators ficam visíveis ao parent. Hooks lifecycle: onRequest → preParsing → preValidation → preHandler → preSerialization → onSend → onResponse (+ onError).
Plugin Logística — db.plugin.ts:
import fp from 'fastify-plugin';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { FastifyPluginAsync } from 'fastify';
declare module 'fastify' {
interface FastifyInstance {
db: ReturnType<typeof drizzle>;
}
}
const dbPlugin: FastifyPluginAsync<{ url: string }> = async (fastify, opts) => {
const sql = postgres(opts.url, { max: 20 });
const db = drizzle(sql);
fastify.decorate('db', db);
fastify.addHook('onClose', async () => { await sql.end(); });
};
export default fp(dbPlugin, { name: 'db', dependencies: [] });
Module augmentation (declare module 'fastify') entrega tipagem fastify.db.select()... em todo lugar. onClose hook fecha pool no graceful shutdown.
Plugin com dependencies (auth depende de db):
const authPlugin: FastifyPluginAsync = async (fastify) => {
fastify.decorateRequest('user', null);
fastify.addHook('preHandler', async (req) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return;
const user = await fastify.db.query.users.findFirst({
where: eq(users.token, token),
});
(req as any).user = user;
});
};
export default fp(authPlugin, { name: 'auth', dependencies: ['db'] });
dependencies: ['db'] garante load order; runtime error se db não foi registrado antes.
Encapsulated routes (sub-app pattern):
// routes/orders.ts
const orderRoutes: FastifyPluginAsync = async (fastify) => {
fastify.get('/', async () => fastify.db.select().from(orders));
fastify.post('/', { schema: createOrderSchema }, async (req) => { /* ... */ });
};
// app.ts
await app.register(orderRoutes, { prefix: '/orders' });
await app.register(adminRoutes, { prefix: '/admin' }); // auth hook só aqui
Cada prefix registrado fica encapsulated — auth hook em adminRoutes não atinge /orders.
Typed schemas com Zod (alternativa TypeBox) — fastify-zod 1+:
import { z } from 'zod';
const orderSchema = {
body: z.object({ items: z.array(z.string()).min(1) }),
response: { 200: z.object({ id: z.string() }) },
};
fastify.post('/orders', { schema: orderSchema }, async (req) => {
// req.body fully typed
return { id: 'order-123' };
});
Compile-time + runtime validation + auto OpenAPI via @fastify/swagger.
Hono middleware composition (Hono 4+):
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { jwt } from 'hono/jwt';
const app = new Hono<{ Variables: { user: User } }>();
app.use('*', logger());
app.use('/api/*', cors({ origin: 'https://logistica.example.com' }));
const adminApp = new Hono<{ Variables: { user: User } }>();
adminApp.use('*', jwt({ secret: process.env.JWT_SECRET! }));
adminApp.use('*', async (c, next) => {
const user = c.get('user');
if (!user.isAdmin) return c.json({ error: 'Forbidden' }, 403);
await next();
});
adminApp.get('/users', async (c) => c.json(await db.select().from(users)));
app.route('/admin', adminApp);
Type-safe Variables: c.set('user', user) + c.get('user') tipados via generic na construção.
Hono custom middleware authoring:
import { createMiddleware } from 'hono/factory';
export const tenantMiddleware = createMiddleware<{ Variables: { tenantId: string } }>(
async (c, next) => {
const subdomain = c.req.header('host')?.split('.')[0];
if (!subdomain) return c.json({ error: 'Tenant required' }, 400);
c.set('tenantId', subdomain);
await next();
},
);
app.use('/api/*', tenantMiddleware);
Type-safe DI alternativas 2026:
Decisão: framework-native vence até ~50 routes; Awilix em scale; NestJS apenas quando time prefere framework-driven (custo: 1k+ linhas extra de boilerplate).
Plugin testing (vitest + app.inject()):
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import dbPlugin from '../src/plugins/db';
import orderRoutes from '../src/routes/orders';
describe('orders routes', () => {
let app: FastifyInstance;
beforeEach(async () => {
app = Fastify({ logger: false });
await app.register(dbPlugin, { url: process.env.TEST_DB_URL! });
await app.register(orderRoutes, { prefix: '/orders' });
await app.ready();
});
afterEach(async () => { await app.close(); });
it('GET /orders returns list', async () => {
const res = await app.inject({ method: 'GET', url: '/orders' });
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body)).toEqual([]);
});
});
app.inject() faz request in-process sem HTTP overhead; afterEach com app.close() evita state leak entre tests.
Logística applied stack:
db, auth, tenant, audit, metrics, errorHandler./orders, /couriers, /admin, /webhooks.tenantMiddleware + JWT verify + Drizzle direct.app.inject() para routes; Testcontainers Postgres para db plugin integration.@fastify/swagger + @fastify/swagger-ui.Anti-patterns observados:
fastify-plugin wrapper quando precisa expor decorators (encapsulation esconde; "decorator not found" em runtime).fastify-plugin em route plugins (defeats encapsulation; routes vazam pro escopo global).declare module augmentation (sem types; runtime funciona, dev experience péssimo).onRequest em hot path com sync DB call (bloqueia event loop; use async + cache).c.set('user', x) sem typed Variables generic (runtime ok; tipos perdidos).dependencies: [...] declarado (race condition; depende da registration order).app.inject() em integration tests sem cleanup (tests subsequentes herdam state; afterEach(app.close())).await next() esquecido (request hangs; ESLint rule hono/no-missing-next).Cruza com 02-09 §2.x (Postgres + Drizzle integration via plugin), 02-13 §2.x (auth plugins JWT/session), 03-01 §2.x (testing strategy via app.inject()), 02-19 §2.x (i18n middleware composition), 03-07 §2.x (observability hooks onRequest/onResponse).
Você precisa, sem consultar:
Reescrever Logística API sobre framework, duas implementações paralelas: Fastify e Hono. Comparar.
POST /auth/login → JWT mockado.GET /orders lista paginada (auth obrigatória).POST /orders cria pedido (auth + validation).GET /orders/:id detalhe.POST /orders/:id/events registra evento.GET /orders/export.csv streaming.GET /healthz, GET /metrics.X-Tenant-Id.customerName obrigatório, total > 0, items array não vazio./docs em ambos.OrderNotFound, OrderAlreadyDelivered) viram 404/409 com Problem Details.requestId, tenantId, method, path, status, latency_ms.console.log em código de produção.server-fastify/, server-hono/).autocannon em ambos. Reportar:
GET /orders./docs).http. Hooks e plugins refletem fases do processamento que vimos.Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Qual a diferença chave entre Hono e Fastify em decisão de runtime?
Q2Por que NestJS frequentemente é overhead injustificado em microservices simples?
Q3No modelo de encapsulation do Fastify, qual a diferença entre `fastify-plugin` (`fp(fn)`) e um plugin sem wrapper?
Q4Em RFC 9457 Problem Details, qual status HTTP corresponde a 'autenticado mas sem permissão'?
Q5Qual a pegadinha clássica de CORS com `Access-Control-Allow-Origin: *`?
Destrava
02-08 é prereq dos seguintes módulos: