@varbyte/nest-worker
v0.1.0
Published
NestJS-inspired micro framework for Cloudflare Workers with D1 support
Maintainers
Readme
nest-worker 🪺
Mini framework estilo NestJS para Cloudflare Workers con soporte nativo para D1.
Características
- Decoradores —
@Controller,@Get,@Post,@Body,@Param,@D1, etc. - Módulos — organiza tu app en módulos con
@Module - Inyección de dependencias —
@Injectable+ constructor injection - D1 integrado —
@D1()inyecta el binding,D1RepositoryyQueryBuilderlistos para usar - Middlewares — CORS, logger, rate limiting, bearer auth incluidos
- Excepciones HTTP —
NotFoundException,BadRequestException, etc. - Cero dependencias en runtime — solo
reflect-metadata
Inicio rápido
1. Instalar dependencias
npm install reflect-metadata
npm install -D typescript wrangler @cloudflare/workers-types2. Configurar tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"types": ["@cloudflare/workers-types"]
}
}3. Crear tu Worker
// worker.ts
import 'reflect-metadata';
import { Module, createApplication, cors } from './src/index';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
class AppModule {}
const app = createApplication(AppModule);
app.use(cors());
export default app.handler;4. Configurar wrangler.toml
name = "my-nest-worker"
main = "worker.ts"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "YOUR_D1_DATABASE_ID"Decoradores
Módulos
@Module({
imports: [OtherModule], // importar otros módulos
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // opcional
})
class AppModule {}Controladores y rutas
@Controller('users') // prefijo de ruta → /users
export class UsersController {
constructor(private svc: UsersService) {}
@Get() // GET /users
getAll() { ... }
@Get(':id') // GET /users/:id
getOne(@Param('id') id: string) { ... }
@Post() // POST /users
create(@Body() body: CreateUserDto) { ... }
@Put(':id') // PUT /users/:id
update(@Param('id') id: string, @Body() body: UpdateUserDto) { ... }
@Delete(':id') // DELETE /users/:id
remove(@Param('id') id: string) { ... }
}Parámetros de handler
| Decorador | Descripción |
|-----------|-------------|
| @Body() | Body completo del request (JSON) |
| @Body('campo') | Un campo específico del body |
| @Param('id') | Path parameter |
| @Query('page') | Query string parameter |
| @Headers('authorization') | Header específico |
| @Req() | Request completo |
| @D1() | Binding D1 (env.DB por defecto) |
| @D1('MY_DB') | Binding D1 con clave personalizada |
| @Env() | Objeto env completo |
| @Env('MY_SECRET') | Variable de entorno específica |
Servicios
@Injectable()
export class UsersService {
async findAll(db: D1Database) {
const repo = new D1Repository<User>(db, 'users');
return repo.findAll();
}
}D1 — Base de datos
D1Repository
Clase base con operaciones CRUD listas para usar:
const repo = new D1Repository<User>(db, 'users');
// CRUD básico
await repo.findAll();
await repo.findById(1);
await repo.findWhere({ role: 'admin' });
await repo.findOneWhere({ email: '[email protected]' });
await repo.create({ name: 'Alice', email: '[email protected]', role: 'user' });
await repo.update(1, { name: 'Alice Updated' });
await repo.delete(1);
await repo.count({ role: 'admin' });
// Queries personalizadas
await repo.raw('SELECT * FROM users WHERE created_at > ?', '2024-01-01');
await repo.rawFirst('SELECT * FROM users WHERE email = ?', '[email protected]');QueryBuilder
Para queries más complejas con interfaz fluida:
import { QueryBuilder } from './src/index';
const users = await new QueryBuilder<User>(db, 'users')
.select('id', 'name', 'email')
.where('role', 'admin')
.where('name', 'A%', 'LIKE')
.orderBy('created_at', 'DESC')
.limit(10)
.offset(20)
.all();
const count = await new QueryBuilder(db, 'users')
.where('role', 'admin')
.count();Inyectar D1 en controladores
@Get()
async getAll(@D1() db: D1Database) {
// db = env.DB automáticamente
const repo = new D1Repository(db, 'users');
return repo.findAll();
}
// Con binding personalizado
@Get()
async getData(@D1('ANALYTICS_DB') db: D1Database) {
// db = env.ANALYTICS_DB
}Middlewares
Globales (en la app)
app
.use(logger())
.use(cors({ origin: 'https://mi-dominio.com' }))
.use(rateLimit({ windowMs: 60_000, max: 100 }));Por controlador o ruta
@Controller('admin')
@UseMiddleware(bearerAuth({ tokenEnvKey: 'ADMIN_TOKEN' })) // aplica a todas las rutas
export class AdminController {
@Delete(':id')
@UseMiddleware(bearerAuth({ staticToken: 'super-secret' })) // solo esta ruta
remove(@Param('id') id: string) { ... }
}Middlewares disponibles
// CORS
cors({ origin: '*', methods: ['GET', 'POST'], credentials: false })
// Logger (consola)
logger()
// Rate limiting por IP
rateLimit({ windowMs: 60_000, max: 60 })
// Bearer Token auth
bearerAuth({ tokenEnvKey: 'API_SECRET' }) // lee env.API_SECRET
bearerAuth({ staticToken: 'mi-token' }) // token fijo (dev only)Middleware personalizado
import { MiddlewareFn } from './src/index';
const myMiddleware: MiddlewareFn = async (req, env) => {
const token = req.headers.get('X-Api-Key');
if (!token) {
return new Response('No autorizado', { status: 401 });
}
// Si no retorna Response, continúa al siguiente middleware/handler
};Excepciones HTTP
import {
NotFoundException,
BadRequestException,
UnauthorizedException,
ForbiddenException,
ConflictException,
InternalServerErrorException,
HttpException,
} from './src/index';
// En cualquier handler o servicio
throw new NotFoundException('Usuario no encontrado');
throw new BadRequestException('Email inválido', { field: 'email' });
throw new HttpException('Error custom', 422);
// El framework las captura y responde automáticamente con el status correctoRespuestas del handler
El framework convierte automáticamente lo que retornas:
| Retorno | Respuesta |
|---------|-----------|
| objeto / array | 200 JSON |
| string | 200 text/plain |
| undefined / null | 204 No Content |
| Response | Se usa tal cual |
| throw HttpException | Status correspondiente en JSON |
Comandos D1
# Crear base de datos
wrangler d1 create my-app-db
# Ejecutar migración
wrangler d1 execute my-app-db --file=./migrations/001_init.sql
# Seed de datos
wrangler d1 execute my-app-db --file=./migrations/002_seed.sql
# Query directa
wrangler d1 execute my-app-db --command="SELECT * FROM users"
# Dev local (D1 incluido)
wrangler dev
# Deploy
wrangler deploy
# Secretos
wrangler secret put API_SECRETEstructura de proyecto recomendada
my-worker/
├── src/ # Framework (esta librería)
├── migrations/
│ ├── 001_init.sql
│ └── 002_seed.sql
├── modules/
│ ├── users/
│ │ ├── users.controller.ts
│ │ ├── users.service.ts
│ │ └── users.module.ts
│ └── auth/
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ └── auth.module.ts
├── worker.ts # Entrypoint principal
├── wrangler.toml
├── tsconfig.json
└── package.jsonEjemplo con módulos separados
// modules/users/users.module.ts
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
// worker.ts
@Module({
imports: [UsersModule, AuthModule],
})
class AppModule {}Licencia
MIT
