@jamx-framework/server
v1.0.0
Published
JAMX Framework — HTTP Server
Maintainers
Readme
@jamx-framework/server
Descripción
Servidor HTTP de JAMX Framework. Proporciona una base ligera y modular para construir aplicaciones server-side, con pipeline de middlewares, soporte para HMR (Hot Module Replacement), validación de requests, rate limiting, seguridad (CSRF, headers), y manejo de archivos estáticos. Está construido sobre node:http nativo sin dependencias externas pesadas.
Cómo funciona
El servidor implementa un modelo de pipeline de middlewares:
- Request → Middleware Pipeline: Cada request pasa por una cadena de middlewares
- Request Dispatcher: El dispatcher (ej: FileRouter) enruta al handler correcto
- Response: El handler envía la respuesta a través de
JamxResponse - Eventos: El servidor emite eventos para logging, monitoreo, HMR
Componentes principales
JamxServer (src/http/server.ts)
Servidor HTTP principal:
create(options): Factory method para crear instanciastart(): Inicia el servidorstop(): Detiene gracefulmenteuse(middleware): Añade middleware dinámicamenteon(event, handler): Suscribe a eventoshttpServer: Acceso alhttp.Servernativo
JamxRequest (src/http/request.ts)
Wrapper de http.IncomingMessage con utilidades:
method,url,path,query,params,headersbody: Parseado automáticamente porbodyParserlocals: Objeto para datos entre middlewares
JamxResponse (src/http/response.ts)
Wrapper de http.ServerResponse con API fluida:
status(code),header(name, value),json(body),text(body)send(body),redirect(url),notFound(),noContent()raw: Acceso alServerResponsenativo
MiddlewarePipeline (src/middleware/pipeline.ts)
Gestiona la cadena de ejecución de middlewares:
use(middleware): Añade middlewareclone(): Crea copia para aislamiento por requestexecute(req, res): Ejecuta pipeline completo
Middlewares built-in
bodyParser: Parse JSON, URL-encoded, text bodiescors: Configura CORS headersrequestId: AñadeX-Request-IDúnicostaticFiles: Sirve archivos estáticosglobalErrorHandler: Captura errores no manejadosrateLimit: Limita requests por IP/cliente
HMR (Hot Module Replacement)
HmrServer: Coordina HMR entre servidor y clienteFileWatcher: Observa cambios en archivosHmrWebSocketServer: WebSocket para push de cambiosgenerateClientScript: Genera script de cliente para HMR
Request Dispatcher (src/router/dispatcher.ts)
Interface para enrutadores:
dispatch(req, res): Método que deben implementar routers como FileRouter
Validación (src/validation/parse-body.ts)
parseBody(req): Parsea body según Content-TypeparseQuery(url): Parsea query stringparseParams(route, path): Extrae params de ruta
Webhooks (src/webhooks/)
WebhookSender: Envía webhooks con retrywebhookVerifier: Verifica signatures de webhooks
Uso básico
Servidor simple
import { JamxServer } from '@jamx-framework/server';
const server = await JamxServer.create();
server.use(async (req, res) => {
res.json({ message: 'Hello World' });
});
await server.start();
console.log(`Server running on http://${server.address.host}:${server.address.port}`);
await server.stop();Con middleware de body parsing
import { JamxServer, bodyParser } from '@jamx-framework/server';
const server = await JamxServer.create();
server.use(bodyParser()); // parsea req.body automáticamente
server.use(async (req, res) => {
if (req.method === 'POST') {
const data = req.body as { name: string };
res.json({ received: data.name });
}
});
await server.start();Con CORS
import { JamxServer, cors } from '@jamx-framework/server';
const server = await JamxServer.create();
server.use(cors({
origin: ['https://example.com', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
headers: ['Content-Type', 'Authorization'],
}));
await server.start();Con archivos estáticos
import { JamxServer, staticFiles } from '@jamx-framework/server';
const server = await JamxServer.create();
// Servir archivos estáticos desde /public
server.use(staticFiles({
root: './public',
cacheControl: 'public, max-age=3600',
}));
// API después de estáticos
server.use(async (req, res) => {
res.json({ api: true });
});
await server.start();Con rate limiting
import { JamxServer, rateLimit } from '@jamx-framework/server';
const server = await JamxServer.create();
server.use(rateLimit({
max: 100, // 100 requests por ventana
window: '1m', // 1 minuto
keyBy: 'ip', // o 'user-id', 'session-id', etc.
}));
await server.start();Con FileRouter
import { JamxServer, FileRouter } from '@jamx-framework/server';
const server = await JamxServer.create();
const router = new FileRouter({ projectRoot: './' });
server.use(router.dispatch.bind(router));
await server.start();Con middleware personalizado
import { JamxServer } from '@jamx-framework/server';
const server = await JamxServer.create();
// Logging middleware
server.use(async (req, res, next) => {
console.log(`${req.method} ${req.path}`);
const start = Date.now();
// Capturar respuesta
const originalSend = res.send.bind(res);
res.send = (body) => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`);
return originalSend(body);
};
next();
});
// Auth middleware
server.use(async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (token) {
try {
const user = await verifyToken(token);
req.locals.user = user;
} catch (err) {
// token inválido, continuar sin usuario
}
}
next();
});
// Handler principal
server.use(async (req, res) => {
const user = req.locals.user;
res.json({ user: user?.id ?? null });
});
await server.start();Con error handling
import { JamxServer, globalErrorHandler } from '@jamx-framework/server';
const server = await JamxServer.create();
// Middleware que puede lanzar errores
server.use(async (req, res) => {
if (Math.random() > 0.5) {
throw new Error('Random error');
}
res.json({ ok: true });
});
// Error handler (debe ser el último)
server.use(globalErrorHandler({
// En desarrollo, retorna stack trace
// En producción, retorna mensaje genérico
showStackTrace: process.env.NODE_ENV === 'development',
}));
await server.start();Con HMR (desarrollo)
import { JamxServer, HmrServer } from '@jamx-framework/server';
const server = await JamxServer.create({
isDev: true,
});
const hmr = new HmrServer({
server,
projectRoot: './',
port: 3000,
});
await hmr.start();
await server.start();Eventos del servidor
import { JamxServer } from '@jamx-framework/server';
const server = await JamxServer.create();
server.on('listening', (event) => {
console.log(`Server listening on ${event.host}:${event.port}`);
});
server.on('request', (event) => {
console.log(`${event.method} ${event.path} → ${event.status} (${event.durationMs}ms)`);
});
server.on('error', (event) => {
console.error('Server error:', event.error);
});
server.on('stopped', () => {
console.log('Server stopped');
});
await server.start();Graceful shutdown
import { JamxServer } from '@jamx-framework/server';
const server = await JamxServer.create({
shutdownTimeout: 10000, // 10 segundos
});
await server.start();
// Manejar señales de terminación
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully...');
await server.stop();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('SIGINT received, shutting down gracefully...');
await server.stop();
process.exit(0);
});API Reference
JamxServer
Factory method
static async create(options?: ServerOptions): Promise<JamxServer>Crea una nueva instancia del servidor.
ServerOptions:
interface ServerOptions {
port?: number; // Puerto (default: 3000)
host?: string; // Host (default: 'localhost')
dispatcher?: RequestDispatcher; // Enrutador (default: StubDispatcher)
middlewares?: Middleware[]; // Middlewares adicionales
isDev?: boolean; // Modo desarrollo (default: false)
shutdownTimeout?: number; // Timeout de shutdown en ms (default: 5000)
}Métodos de ciclo de vida
start(): Promise<void>Inicia el servidor. Retorna una promesa que se resuelve cuando el servidor está escuchando.
stop(): Promise<void>Detiene el servidor gracefulmente. Espera a que las requests en curso terminen antes de cerrar.
Middleware management
use(middleware: Middleware): thisAñade un middleware al pipeline. Puede llamarse antes o después de start().
on(handler: ServerEventHandler): thisSuscribe un handler a eventos del servidor.
Getters
readonly isListening: booleantrue si el servidor está escuchando conexiones.
readonly address: { port: number; host: string }Dirección en la que el servidor está escuchando.
readonly httpServer: http.Server | nullInstancia subyacente de http.Server de Node.js.
JamxRequest
Propiedades
method: string; // Método HTTP (GET, POST, etc.)
url: string; // URL completa con query string
path: string; // Solo el path (sin query)
query: Record<string, string>; // Query parameters parseados
params: Record<string, string>; // Parámetros de ruta (por dispatcher)
headers: Record<string, string>; // Headers (normalizados a minúsculas)
body: unknown; // Cuerpo parseado (por bodyParser)
locals: Record<string, unknown>; // Datos para middlewaresMétodos
getHeader(name: string): string | undefinedObtiene un header específico (case-insensitive).
JamxResponse
Métodos de respuesta
status(code: number): thisEstablece el código de estado HTTP.
header(name: string, value: string): thisAñade un header a la respuesta.
json(body: unknown, status?: number): voidEnvía respuesta JSON. Establece Content-Type: application/json.
text(body: string, status?: number): voidEnvía respuesta de texto plano.
send(body: string | Buffer, status?: number): voidEnvía cuerpo crudo (string o Buffer).
redirect(url: string, status?: number): voidRedirige a otra URL (por defecto 302).
notFound(message?: string): voidResponde con 404 Not Found.
noContent(): voidResponde con 204 No Content.
raw: http.ServerResponseAcceso directo al ServerResponse nativo de Node.js.
Getters
readonly sent: booleantrue si ya se envió una respuesta.
Middleware
Tipo
type Middleware = (
req: JamxRequest,
res: JamxResponse,
next: NextFn
) => Promise<void> | void;NextFn
type NextFn = () => Promise<void> | void;Función que debe llamarse para continuar al siguiente middleware.
Ejemplo de middleware
function loggingMiddleware(req: JamxRequest, res: JamxResponse, next: NextFn) {
console.log(`${req.method} ${req.path}`);
next();
}
function authMiddleware(req: JamxRequest, res: JamxResponse, next: NextFn) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
res.json({ error: 'Unauthorized' }, 401);
return;
}
// verificar token...
next();
}MiddlewarePipeline
Métodos
use(middleware: Middleware): voidAñade un middleware al final del pipeline.
clone(): MiddlewarePipelineCrea una copia del pipeline con los mismos middlewares. Usado para aislar cada request.
execute(req: JamxRequest, res: JamxResponse): Promise<void>Ejecuta todos los middlewares en secuencia.
RequestDispatcher
Interface
interface RequestDispatcher {
dispatch(req: JamxRequest, res: JamxResponse): Promise<void>;
}Cualquier router (FileRouter, StubDispatcher) implementa esta interface.
StubDispatcher
Dispatcher por defecto que responde 404 a todo. Útil para tests o como fallback.
Built-in Middlewares
bodyParser
function bodyParser(options?: {
limit?: string | number; // límite de tamaño (default: '1mb')
json?: boolean; // parsear JSON (default: true)
urlencoded?: boolean; // parsear URL-encoded (default: true)
text?: boolean; // parsear texto (default: false)
}): MiddlewareParsea el cuerpo de la request según Content-Type.
Content-Type soportados:
application/json→req.body= objeto parseadoapplication/x-www-form-urlencoded→req.body= objetotext/plain→req.body= string
cors
function cors(options?: {
origin: string | string[]; // orígenes permitidos
methods?: string[]; // métodos permitidos
headers?: string[]; // headers permitidos
credentials?: boolean; // Access-Control-Allow-Credentials
}): MiddlewareConfigura CORS headers.
requestId
function requestId(): MiddlewareAñade header X-Request-ID con UUID único a cada request.
staticFiles
function staticFiles(options?: {
root: string; // directorio raíz
cacheControl?: string; // Cache-Control header
index?: boolean | string; // servir index.html por defecto
extensions?: string[]; // extensiones a probar
}): MiddlewareSirve archivos estáticos desde disco.
globalErrorHandler
function globalErrorHandler(options?: {
showStackTrace?: boolean; // mostrar stack en respuesta
logger?: (err: unknown) => void; // logger personalizado
}): MiddlewareCaptura errores no manejados y responde con 500.
rateLimit
function rateLimit(options?: {
max: number; // max requests por ventana
window: string | number; // ventana de tiempo ('1m', '1h', etc.)
keyBy: 'ip' | 'user-id' | 'session-id' | ((req) => string); // clave
storage?: 'memory' | 'redis'; // backend de almacenamiento
}): MiddlewareLimita el número de requests por cliente.
HMR
HmrServer
class HmrServer {
constructor(options: HmrServerOptions);
start(): Promise<void>;
stop(): Promise<void>;
}Coordina Hot Module Replacement.
HmrServerOptions:
interface HmrServerOptions {
server: JamxServer; // instancia del servidor
projectRoot: string; // raíz del proyecto
port: number; // puerto del servidor
wsPort?: number; // puerto WebSocket (default: port+1)
}FileWatcher
class FileWatcher {
constructor(projectRoot: string);
onChange(handler: (changes: FileChange[]) => void): void;
start(): void;
stop(): void;
}Observa cambios en archivos.
FileChange:
interface FileChange {
path: string;
kind: ChangeKind; // 'create' | 'modify' | 'delete' | 'rename'
}HmrWebSocketServer
class HmrWebSocketServer {
constructor(server: http.Server, port: number);
broadcast(message: HmrMessage): void;
start(): void;
stop(): void;
}WebSocket server para push de cambios a clientes.
HmrMessage:
interface HmrMessage {
type: 'reload' | 'update' | 'error';
path?: string;
data?: unknown;
}generateClientScript
function generateClientScript(options: {
wsUrl: string;
projectRoot: string;
isDev: boolean;
}): stringGenera el script JavaScript que el cliente ejecuta para HMR.
Webhooks
WebhookSender
class WebhookSender {
constructor(options: {
url: string;
secret?: string; // para verificar signature
retries?: number; // reintentos (default: 3)
timeout?: number; // timeout en ms (default: 5000)
});
async send(payload: unknown, options?: SendWebhookOptions): Promise<WebhookDelivery>;
}Envía webhooks con retry automático.
WebhookDelivery:
interface WebhookDelivery {
id: string;
status: 'pending' | 'sent' | 'failed';
attempts: number;
response?: http.IncomingMessage;
error?: Error;
}webhookVerifier
function webhookVerifier(secret: string): (req: JamxRequest) => booleanRetorna un middleware que verifica la signature de un webhook.
Flujo interno
Ciclo de vida de una request
// 1. Servidor recibe conexión HTTP
server = http.createServer(async (rawReq, rawRes) => handleRequest(rawReq, rawRes));
// 2. handleRequest crea wrappers JAMX
const req = new JamxRequest(rawReq);
const res = new JamxResponse(rawRes);
// 3. Clonar pipeline (aislamiento por request)
const pipeline = this.pipeline.clone();
// 4. Añadir dispatcher como último middleware
pipeline.use(async (req, res) => {
await this.dispatcher.dispatch(req, res);
});
// 5. Ejecutar pipeline
await pipeline.execute(req, res);
// 6. Si no se respondió → 404
if (!res.sent && !rawRes.writableEnded) {
res.notFound();
}
// 7. Emitir evento de request
this.emit({
type: 'request',
method: req.method,
path: req.path,
status: rawRes.statusCode,
durationMs: Date.now() - startTime,
});Pipeline de middlewares
class MiddlewarePipeline {
private middlewares: Middleware[] = [];
use(mw: Middleware) {
this.middlewares.push(mw);
}
clone(): MiddlewarePipeline {
const clone = new MiddlewarePipeline();
clone.middlewares = [...this.middlewares];
return clone;
}
async execute(req: JamxRequest, res: JamxResponse) {
let index = 0;
const next = (): Promise<void> => {
if (index >= this.middlewares.length) return Promise.resolve();
const mw = this.middlewares[index++];
return Promise.resolve(mw(req, res, next)).then(next);
};
await next();
}
}Construcción del pipeline
private buildPipeline(extraMiddlewares: Middleware[]): MiddlewarePipeline {
const pipeline = new MiddlewarePipeline();
// 1. Middlewares built-in
pipeline.use(requestId()); // X-Request-ID
pipeline.use(bodyParser()); // parse body
// 2. Middlewares del usuario
for (const mw of extraMiddlewares) {
pipeline.use(mw);
}
return pipeline;
}Request lifecycle completo
// Ejemplo con todos los componentes
const server = await JamxServer.create({
port: 3000,
middlewares: [
cors({ origin: '*' }),
rateLimit({ max: 100, window: '1m' }),
],
});
// 1. Request entra
// 2. requestId → añade X-Request-ID
// 3. bodyParser → parsea body
// 4. cors → configura CORS headers
// 5. rateLimit → verifica límite
// 6. user middlewares (logging, auth, etc.)
// 7. dispatcher.dispatch → enruta a handler
// 8. handler envía respuesta
// 9. Si no se respondió → 404
// 10. Emitir evento 'request'Configuración
Opciones del servidor
const server = await JamxServer.create({
port: 8080,
host: '0.0.0.0',
isDev: true,
shutdownTimeout: 10000,
middlewares: [customMiddleware],
dispatcher: fileRouter,
});Variables de entorno
PORT: Puerto del servidor (sobreescribeoptions.port)HOST: Host del servidor (sobreescribeoptions.host)NODE_ENV: Modo desarrollo/producción (afecta logs, errores)
Testing
Tests unitarios
import { JamxServer, bodyParser } from '@jamx-framework/server';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('Server', () => {
let server: JamxServer;
beforeEach(async () => {
server = await JamxServer.create({
port: 0, // puerto aleatorio
});
});
afterEach(async () => {
await server.stop();
});
it('should start and stop', async () => {
await server.start();
expect(server.isListening).toBe(true);
await server.stop();
expect(server.isListening).toBe(false);
});
it('should handle requests', async () => {
server.use(async (req, res) => {
res.json({ method: req.method, path: req.path });
});
await server.start();
const port = server.address.port;
const response = await fetch(`http://localhost:${port}/test`);
const data = await response.json();
expect(data.method).toBe('GET');
expect(data.path).toBe('/test');
});
it('should emit events', async () => {
const events: any[] = [];
server.on('listening', (e) => events.push(e));
server.on('request', (e) => events.push(e));
server.on('stopped', (e) => events.push(e));
await server.start();
await fetch(`http://localhost:${server.address.port}/ping`);
await server.stop();
expect(events.some(e => e.type === 'listening')).toBe(true);
expect(events.some(e => e.type === 'request')).toBe(true);
expect(events.some(e => e.type === 'stopped')).toBe(true);
});
});Tests con TestServer
import { createTestServer } from '@jamx-framework/testing';
const testServer = createTestServer();
testServer.get('/api/test', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
});
await testServer.start();
const client = new TestClient(testServer.url);
const response = await client.get('/api/test');
expect(response.status).toBe(200);
expect(response.json()).toEqual({ ok: true });
await testServer.stop();Consideraciones de rendimiento
Pipeline optimizado
- Los middlewares se ejecutan en secuencia, no en paralelo
pipeline.clone()crea una copia ligera por request (shallow copy de array)- No hay overhead significativo por request
Sin dependencias externas
- Solo usa
node:httpnativo - Sin Express/Fastify → menos memoria, menos CPU
- Ideal para serverless (Lambda, Cloudflare Workers via adapters)
HMR overhead
- En desarrollo, HMR agrega WebSocket server y file watcher
- En producción, HMR está deshabilitado
- El overhead es mínimo (~1-2MB de memoria)
Body parsing
bodyParsercarga el body completo en memoria- Para uploads grandes, usar
staticFileso streaming - Límite por defecto: 1MB (configurable)
Rate limiting
- Por defecto usa memoria (Map)
- Para múltiples instancias, usar Redis storage
- Limpia automáticamente entradas expiradas
Seguridad
Headers de seguridad
El servidor no agrega security headers por defecto. Usar middleware:
import { helmet } from '@jamx-framework/security'; // (hipotético)
server.use(helmet());O manualmente:
server.use((req, res, next) => {
res.header('X-Content-Type-Options', 'nosniff');
res.header('X-Frame-Options', 'DENY');
res.header('X-XSS-Protection', '1; mode=block');
next();
});CSRF
No incluido en core. Usar @jamx-framework/auth o middleware externo.
Validación de input
El servidor no valida automáticamente. Usar @jamx-framework/validator:
import { validate } from '@jamx-framework/validator';
server.use(async (req, res, next) => {
if (req.method === 'POST') {
const result = validate(req.body, userSchema);
if (!result.valid) {
res.json({ errors: result.errors }, 400);
return;
}
req.body = result.value; // datos validados y casteados
}
next();
});Limitaciones
Sin soporte nativo para:
- WebSockets (usar
@jamx-framework/realtimecon adapter) - HTTP/2 (solo HTTP/1.1)
- TLS/SSL (usir reverse proxy como Nginx)
- Sessions (usar
@jamx-framework/authcon JWT o DB)
Single-threaded
- Node.js es single-threaded
- Para CPU-intensive tasks, usar Worker Threads o external services
- Para I/O, Node.js es no-blocking por defecto
No incluye:
- ORM (usar
@jamx-framework/db) - Template engine (usar
@jamx-framework/renderer) - Authentication (usar
@jamx-framework/auth)
Integración con otros paquetes
Con @jamx-framework/router
import { FileRouter } from '@jamx-framework/router';
const router = new FileRouter({ projectRoot: './' });
const server = await JamxServer.create();
server.use(router.dispatch.bind(router));Con @jamx-framework/config
import { ConfigManager } from '@jamx-framework/config';
const configManager = new ConfigManager();
await configManager.load('./');
const server = await JamxServer.create({
port: configManager.section('server').port,
host: configManager.section('server').host,
});Con @jamx-framework/auth
import { authMiddleware } from '@jamx-framework/auth';
server.use(authMiddleware());Con @jamx-framework/metrics
import { Metrics } from '@jamx-framework/metrics';
server.on('request', (event) => {
Metrics.histogram('http.request.duration', event.durationMs, {
method: event.method,
path: event.path,
status: event.status,
});
});Ejemplo completo
// server/index.ts
import { JamxServer, bodyParser, cors, rateLimit, staticFiles, requestId } from '@jamx-framework/server';
import { FileRouter } from '@jamx-framework/router';
import { ConfigManager } from '@jamx-framework/config';
import { Metrics } from '@jamx-framework/metrics';
async function main() {
// Cargar configuración
const configManager = new ConfigManager();
await configManager.load('./');
const config = configManager.get();
// Crear servidor
const server = await JamxServer.create({
port: config.server?.port ?? 3000,
host: config.server?.host ?? 'localhost',
isDev: configManager.isDev,
middlewares: [
// CORS
cors({
origin: config.server?.cors?.origins ?? ['http://localhost:3000'],
credentials: true,
}),
// Rate limiting
rateLimit({
max: config.security?.rateLimit?.max ?? 100,
window: config.security?.rateLimit?.window ?? '1m',
keyBy: 'ip',
}),
// Logging con métricas
async (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
Metrics.histogram('http.request.duration', duration, {
method: req.method,
path: req.path,
status: res.statusCode,
});
});
next();
},
// Autenticación (si está configurada)
...(config.auth ? [authMiddleware(config.auth)] : []),
],
});
// Eventos
server.on('listening', () => {
console.log(`Server listening on http://${server.address.host}:${server.address.port}`);
});
server.on('error', (err) => {
console.error('Server error:', err);
});
// Archivos estáticos
server.use(staticFiles({
root: './public',
cacheControl: 'public, max-age=3600',
}));
// API routes con FileRouter
const router = new FileRouter({ projectRoot: './' });
server.use(router.dispatch.bind(router));
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down...');
await server.stop();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('SIGINT received, shutting down...');
await server.stop();
process.exit(0);
});
// Iniciar
await server.start();
}
main().catch(console.error);Referencia rápida
Crear servidor
const server = await JamxServer.create(options);Middlewares comunes
server.use(bodyParser());
server.use(cors({ origin: '*' }));
server.use(rateLimit({ max: 100, window: '1m' }));
server.use(staticFiles({ root: './public' }));
server.use(requestId());
server.use(globalErrorHandler());Añadir middleware personalizado
server.use(async (req, res, next) => {
// lógica
next();
});Eventos
server.on('listening', (e) => console.log(`Listening on ${e.port}`));
server.on('request', (e) => console.log(`${e.method} ${e.path}`));
server.on('error', (e) => console.error(e.error));
server.on('stopped', () => console.log('Stopped'));Control
await server.start();
await server.stop();Acceso a http.Server nativo
const httpServer = server.httpServer;
// puedes añadir listeners personalizados, etc.Archivos importantes
src/http/server.ts- JamxServer principalsrc/http/request.ts- JamxRequestsrc/http/response.ts- JamxResponsesrc/middleware/pipeline.ts- MiddlewarePipelinesrc/middleware/built-in/- Middlewares incluidossrc/router/dispatcher.ts- RequestDispatcher interfacesrc/hmr/- Hot Module Replacementtests/unit/http/server.test.ts- Tests del servidortests/unit/middleware/- Tests de middlewares
Dependencias
@jamx-framework/core- ParaNotFoundExceptiony tipos@jamx-framework/cache- Para rate limit con Redis@jamx-framework/config- Para configuración@jamx-framework/storage- Para static files con S3@jamx-framework/validator- Para validación@types/node- Tipos de Node.jsvitest- Testingrimraf- Limpieza
Scripts del paquete
pnpm build- Compila TypeScript a JavaScriptpnpm dev- Compilación en watch modepnpm test- Ejecuta tests unitariospnpm test:watch- Tests en watch modepnpm type-check- Verifica tipos sin compilarpnpm clean- Limpia archivos compilados
Comparación con Express/Fastify
| Característica | JAMX Server | Express | Fastify | |----------------|-------------|---------|---------| | Peso | ~50KB | ~200KB | ~150KB | | Dependencias | Solo core | muchas | moderadas | | Pipeline | Sí (explicito) | Sí (implícito) | Sí | | TypeScript | Nativo | opcional | opcional | | HMR | Sí (integrado) | No | No | | Validación | Externa | Externa | Externa | | ORM | Externa | Externa | Externa | | Rendimiento | Muy alto | Medio | Alto | | Aprendizaje | Moderado | Fácil | Moderado |
Buenas prácticas
1. Orden de middlewares
// Correcto: bodyParser antes de handlers que usen req.body
server.use(bodyParser());
server.use(async (req, res) => {
// req.body disponible aquí
});
// Incorrecto: bodyParser después
server.use(async (req, res) => {
// req.body undefined aquí
});
server.use(bodyParser());2. Error handling al final
// Middlewares primero
server.use(bodyParser());
server.use(cors());
server.use(auth);
// Error handler al final
server.use(globalErrorHandler());3. Usar async/await
// ✅ Bien
server.use(async (req, res, next) => {
try {
const data = await fetchData();
res.json(data);
} catch (err) {
next(err); // pasar al error handler
}
});
// ❌ No usar callbacks
server.use((req, res, next) => {
fetchData((err, data) => {
if (err) return next(err);
res.json(data);
});
});4. Llamar next() o responder
// ✅ Una de las dos, no ambas
server.use(async (req, res, next) => {
// O respondes:
res.json({ ok: true });
// O llamas next():
// next();
});
// ❌ No hagas ambas
server.use(async (req, res, next) => {
res.json({ ok: true });
next(); // error: headers ya enviados
});5. Usar locals para datos compartidos
// Middleware de autenticación
server.use(async (req, res, next) => {
const user = await verifyToken(req.headers.authorization);
req.locals.user = user; // guardar para siguientes middlewares
next();
});
// Handler
server.use(async (req, res) => {
const user = req.locals.user; // disponible aquí
res.json({ user });
});Preguntas frecuentes
¿Cómo manejar 404?
El servidor responde 404 automáticamente si ningún middleware envía respuesta.
// Para custom 404, añadir middleware al final
server.use(async (req, res) => {
res.notFound('Page not found');
});¿Cómo manejar errores 500?
Usar globalErrorHandler al final del pipeline:
server.use(globalErrorHandler({
showStackTrace: process.env.NODE_ENV === 'development',
}));¿Puedo usar Express middleware?
No directamente. Los middlewares de Express tienen una API diferente (3 args vs 4 args, next como callback). Necesitas un adapter.
¿Cómo servir HTTPS?
El servidor solo soporta HTTP. Usar reverse proxy (Nginx, Caddy) o spdy/http2 externamente.
¿Cómo escalar horizontalmente?
- Usar balanceador de carga (Nginx, HAProxy)
- Asegurar que el estado sea externo (Redis, DB)
- No guardar estado en memoria del servidor
¿Cómo añadir WebSocket?
El servidor no incluye WebSocket. Usar @jamx-framework/realtime:
import { WebSocketServer } from '@jamx-framework/realtime';
const wsServer = new WebSocketServer({ server });
wsServer.on('connection', (socket) => {
socket.on('message', (msg) => {
// manejar mensaje
});
});¿Cómo subir archivos?
Usar busboy o multer externamente, o el middleware multipart de JAMX (si existe).
import { multipart } from '@jamx-framework/server';
server.use(multipart({ limits: { fileSize: 10 * 1024 * 1024 } }));
server.use(async (req, res) => {
const file = req.files?.[0];
if (file) {
await saveFile(file);
}
res.json({ ok: true });
});¿Cómo usar sesiones?
El servidor no incluye sesiones. Usar @jamx-framework/auth con JWT o sesiones en DB.
import { session } from '@jamx-framework/auth';
server.use(session({
secret: process.env.SESSION_SECRET,
store: new RedisStore(),
}));Limitaciones conocidas
No incluye:
- Template rendering (usar
@jamx-framework/renderer) - ORM/database (usar
@jamx-framework/db) - Authentication (usar
@jamx-framework/auth) - Validation (usar
@jamx-framework/validator) - Rate limiting con Redis (solo memoria por defecto)
- WebSockets (usar
@jamx-framework/realtime)
Diseñado para:
- APIs REST/GraphQL
- SSR con renderer
- Microservicios
- Serverless functions (via adapters)
No diseñado para:
- Aplicaciones monolíticas pesadas (usar Next.js/Nuxt)
- WebSockets puros (usar Socket.io)
- FTP/SFTP (usar servidores especializados)
Futuras mejoras
- Soporte para HTTP/2
- WebSocket integrado
- Sesiones nativas
- Template engine integrado
- Metrics integradas (Prometheus endpoint)
- Graceful shutdown mejorado (drenar conexiones)
- Request timeout configurable
- Compression (gzip/brotli)
- Caching (CDN, browser)
Conclusión
@jamx-framework/server es un servidor HTTP minimalista pero potente, ideal para aplicaciones JAMX que necesitan control total sobre el stack. Su diseño modular permite añadir solo las funcionalidades necesarias, manteniendo el footprint pequeño y el rendimiento alto.
