Skip to content

Um roteador simples para Cloudflare Workers. Suporta Middlewares, cache, queries e parâmetro de rota.

License

Notifications You must be signed in to change notification settings

HelenoSalgado/routerworkers

Repository files navigation

RouterWorkers

npm version npm downloads License: MIT TypeScript Cloudflare Workers

Um roteador moderno, minimalista e poderoso para Cloudflare Workers

InstalaçãoInício RápidoDocumentaçãoExemplos


🌟 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 ObjectId

Rotas 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:


🎯 Comparação com Outros Frameworks

Feature RouterWorkers Express Hono
Bundle Size ~29KB ~200KB ~20KB
Response Helpers ✅ 14 ✅ ~10 ✅ ~12
Route Groups
CORS Built-in
Validação Built-in
TypeScript ⚠️
Workers Native
Zero Deps

🛠️ Tecnologias

  • TypeScript 5.9+
  • Cloudflare Workers
  • Rollup (build)
  • Jest (testes)

📊 Status

  • 51 testes passando (100%)
  • Zero dependências
  • Bundle: ~29KB
  • TypeScript strict mode
  • Pronto para produção

🤝 Contribuindo

Contribuições são bem-vindas! Por favor:

  1. Fork o projeto
  2. Crie uma branch para sua feature (git checkout -b feature/AmazingFeature)
  3. Commit suas mudanças (git commit -m 'Add some AmazingFeature')
  4. Push para a branch (git push origin feature/AmazingFeature)
  5. Abra um Pull Request

📄 Licença

MIT © Heleno Salgado


🔗 Links


Feito com ❤️ para Cloudflare Workers

Se este projeto foi útil, considere dar uma ⭐ no GitHub!

About

Um roteador simples para Cloudflare Workers. Suporta Middlewares, cache, queries e parâmetro de rota.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages