routerworkers
v1.1.0
Published
Um roteador moderno e minimalista para Cloudflare Workers com TypeScript, response helpers, route groups, CORS e validação built-in.
Maintainers
Readme
RouterWorkers
Um roteador moderno, minimalista e poderoso para Cloudflare Workers
Instalação • Início Rápido • Documentação • Exemplos
🌟 Features
- ✅ Zero Dependências - Bundle minimalista (~29KB)
- ✅ TypeScript First-class - Tipos completos e inferência automática
- ✅ Response Helpers - 14 métodos semânticos (ok, created, notFound, etc)
- ✅ Route Groups - Organize rotas com prefixos e middlewares compartilhados
- ✅ CORS Built-in - Middleware CORS completo e configurável
- ✅ Validação Built-in - Validador de schemas sem dependências
- ✅ Rotas Aninhadas - Suporte completo (
/users/:id/posts/:postId) - ✅ Error Handlers - Tratamento customizado de erros e 404
- ✅ Cache API - Integração nativa com Cloudflare Cache
- ✅ Middlewares - Globais e por rota
📦 Instalação
npm install routerworkers🚀 Início Rápido
Hello World
import { RouterWorkers } from 'routerworkers';
import type { Req, Res } from 'routerworkers';
export default {
async fetch(request: Request): Promise<Response> {
const app = new RouterWorkers(request);
await app.get('/', (req: Req, res: Res) => {
res.ok({ message: 'Hello World!' });
});
return app.resolve();
}
};API RESTful Completa
import { RouterWorkers, group, cors, validate, schemas } from 'routerworkers';
export default {
async fetch(request: Request): Promise<Response> {
const app = new RouterWorkers(request);
// CORS
await app.use(cors({ origin: 'https://app.example.com' }));
// Error handlers
app.onError((error, req, res) => {
console.error(error);
res.serverError(error.message);
});
app.notFound((req, res) => {
res.notFound('Route not found');
});
// API v1
await group(app, { prefix: '/api/v1' }, async (api) => {
// GET /api/v1/users
await api.get('/users',
validate({ queries: schemas.pagination }),
(req, res) => {
res.ok({ users: [] });
}
);
// GET /api/v1/users/:id
await api.get('/users/:id',
validate({ params: { id: schemas.uuid } }),
(req, res) => {
res.ok({ user: { id: req.params!.id } });
}
);
// POST /api/v1/users
await api.post('/users',
validate({
body: {
name: { type: 'string', required: true },
email: { type: 'email', required: true }
}
}),
(req, res) => {
res.created(req.bodyJson, `/api/v1/users/${req.bodyJson.id}`);
}
);
// DELETE /api/v1/users/:id
await api.delete('/users/:id', (req, res) => {
res.noContent();
});
});
return app.resolve();
}
};📖 Documentação
Response Helpers
RouterWorkers oferece 14 métodos semânticos para respostas HTTP:
Success (2xx)
// 200 OK
res.ok({ users: [] });
// 201 Created
res.created({ id: '123' }, '/users/123');
// 202 Accepted
res.accepted({ jobId: '456', status: 'processing' });
// 204 No Content
res.noContent();Client Errors (4xx)
// 400 Bad Request
res.badRequest('Email is required');
// 401 Unauthorized
res.unauthorized('Token required');
// 403 Forbidden
res.forbidden('Admin access required');
// 404 Not Found
res.notFound('User not found');
// 409 Conflict
res.conflict('Email already exists');
// 422 Unprocessable Entity
res.unprocessable([{ field: 'email', message: 'Invalid' }]);Server Errors (5xx)
// 500 Internal Server Error
res.serverError('Something went wrong');Custom
// JSON customizado
res.json({ custom: true }, 418);
// HTML
res.html('<h1>Hello</h1>');
// Text
res.text('Plain text');Route Groups
Organize rotas com prefixos e middlewares compartilhados:
import { group } from 'routerworkers';
await group(app, { prefix: '/api' }, async (api) => {
// GET /api/users
await api.get('/users', handler);
// POST /api/users
await api.post('/users', handler);
});Com Middlewares
const authMiddleware = async (req, res) => {
if (!req.headers.get('Authorization')) {
res.unauthorized();
}
};
await group(app, {
prefix: '/api',
middlewares: [authMiddleware]
}, async (api) => {
// Todas as rotas aqui exigem autenticação
});Grupos Aninhados
await group(app, { prefix: '/api' }, async (api) => {
await api.group({ prefix: '/v1' }, async (v1) => {
await v1.group({ prefix: '/users' }, async (users) => {
// GET /api/v1/users
await users.get('/', handler);
// GET /api/v1/users/:id
await users.get('/:id', handler);
});
});
});CORS
Desenvolvimento (permite tudo)
import { corsDevMode } from 'routerworkers';
await app.use(corsDevMode());Produção
import { corsProduction } from 'routerworkers';
await app.use(corsProduction([
'https://example.com',
'https://app.example.com'
]));Customizado
import { cors } from 'routerworkers';
await app.use(cors({
origin: 'https://example.com', // ou array ou function
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));Validação
RouterWorkers inclui um validador built-in completo:
import { validate, schemas } from 'routerworkers';
// Validação de body
await app.post('/users',
validate({
body: {
name: { type: 'string', required: true, minLength: 3 },
email: { type: 'email', required: true },
age: { type: 'number', min: 18, max: 120 }
}
}),
(req, res) => {
// req.bodyJson já está validado
res.created(req.bodyJson);
}
);
// Validação de params
await app.get('/users/:id',
validate({ params: { id: schemas.uuid } }),
(req, res) => {
res.ok({ user: { id: req.params!.id } });
}
);
// Validação de queries
await app.get('/users',
validate({ queries: schemas.pagination }),
(req, res) => {
const { page = 1, limit = 10 } = req.queries || {};
res.ok({ users: [], page, limit });
}
);Schemas Pré-definidos
schemas.uuid // UUID válido
schemas.email // Email válido
schemas.url // URL válida
schemas.pagination // { page?: number, limit?: number }
schemas.date // Date válida
schemas.objectId // MongoDB ObjectIdRotas Aninhadas
// Suporte completo a rotas aninhadas
await app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params!;
res.ok({ userId, postId });
});Middlewares
Global
await app.use(async (req, res) => {
console.log(`${req.method} ${req.url}`);
});Por Rota
const authMiddleware = async (req, res) => {
if (!req.headers.get('Authorization')) {
res.unauthorized();
}
};
await app.get('/protected', authMiddleware, (req, res) => {
res.ok({ protected: true });
});Error Handlers
// Handler de erros customizado
app.onError((error, req, res) => {
console.error('[ERROR]', error);
res.serverError(error.message);
});
// Handler 404 customizado
app.notFound((req, res) => {
res.notFound(`Route ${req.url} not found`);
});Cache com Invalidação Automática
O RouterWorkers oferece integração nativa com a Cache API da Cloudflare, com uma solução robusta para o problema de invalidação de cache entre deploys.
O Problema: Cache Persistente
Por padrão, o cache da Cloudflare é persistente. Se você fizer um novo deploy com alterações no código, as respostas para rotas cacheadas podem continuar vindo da versão antiga (em cache), pois a chave do cache (a URL da rota) não mudou. A solução comum, mas tediosa, é adicionar manualmente um número de versão.
A Solução: Versionamento Automático
A solução ideal é usar um identificador único para cada deploy como parte da chave do cache. O RouterWorkers faz isso de forma transparente quando configurado corretamente com o ambiente da Cloudflare.
Passo 1: Configure seu wrangler.toml
Adicione a seguinte configuração ao seu wrangler.toml para que a Cloudflare injete os metadados da versão do seu Worker na variável de ambiente CF_VERSION_METADATA.
# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2023-10-26"
# Habilita a injeção dos metadados da implantação
[version_metadata]
binding = "CF_VERSION_METADATA"Passo 2: Use o ID da Versão na Configuração
No seu código, passe o ID da versão (env.CF_VERSION_METADATA.id) para a configuração do RouterWorkers. Ele será usado para criar uma chave de cache única para o deploy atual.
// src/index.ts
import { RouterWorkers } from 'routerworkers';
import type { Req, Res, ConfigWorker } from 'routerworkers';
// Defina a interface para o seu ambiente
interface Env {
CF_VERSION_METADATA: {
id: string;
timestamp: string;
tag: string;
};
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Obtenha o ID único do deploy
const deploymentId = env.CF_VERSION_METADATA.id;
// Configure o cache com o ID de versionamento
const config: ConfigWorker = {
cache: {
pathname: ['/data'], // A rota que queremos cachear
maxage: '3600', // Tempo de vida do cache (1 hora)
version: deploymentId // Chave de versionamento automático!
}
};
const app = new RouterWorkers(request, config);
// Esta rota será cacheada automaticamente por deploy
await app.get('/data', (req: Req, res: Res) => {
console.log('Executando a lógica da rota (não veio do cache)');
res.ok({
message: 'Estes são dados frescos, servidos diretamente pela função.',
deploymentId: deploymentId
});
});
return app.resolve();
}
};Com essa configuração, a cada novo wrangler deploy, o deploymentId muda, o cache antigo é automaticamente ignorado e seu Worker servirá a nova versão, que será então cacheada.
📝 Exemplos
Veja a pasta examples/ para exemplos completos:
🛠️ Tecnologias
- TypeScript 5.9+
- Cloudflare Workers
- Rollup (build)
- Jest (testes)
📄 Licença
MIT © Heleno Salgado
🔗 Links
Feito com ❤️ para Cloudflare Workers
Se este projeto foi útil, considere dar uma ⭐ no GitHub!
