HTTP Routing
@esengine/server includes a lightweight HTTP routing feature that can share the same port with WebSocket services, making it easy to implement REST APIs.
Quick Start
Section titled “Quick Start”Inline Route Definition
Section titled “Inline Route Definition”The simplest way is to define HTTP routes directly when creating the server:
import { createServer } from '@esengine/server'
const server = await createServer({ port: 3000, http: { '/api/health': (req, res) => { res.json({ status: 'ok', time: Date.now() }) }, '/api/users': { GET: (req, res) => { res.json({ users: [] }) }, POST: async (req, res) => { const body = req.body as { name: string } res.status(201).json({ id: '1', name: body.name }) } } }, cors: true // Enable CORS})
await server.start()File-based Routing
Section titled “File-based Routing”For larger projects, file-based routing is recommended. Create a src/http directory where each file corresponds to a route:
import { defineHttp } from '@esengine/server'
interface LoginBody { username: string password: string}
export default defineHttp<LoginBody>({ method: 'POST', handler(req, res) { const { username, password } = req.body as LoginBody
// Validate user... if (username === 'admin' && password === '123456') { res.json({ token: 'jwt-token-here', userId: 'user-1' }) } else { res.error(401, 'Invalid username or password') } }})import { createServer } from '@esengine/server'
const server = await createServer({ port: 3000, httpDir: './src/http', // HTTP routes directory httpPrefix: '/api', // Route prefix cors: true})
await server.start()// Route: POST /api/logindefineHttp Definition
Section titled “defineHttp Definition”defineHttp is used to define type-safe HTTP handlers:
import { defineHttp } from '@esengine/server'
interface CreateUserBody { username: string email: string password: string}
export default defineHttp<CreateUserBody>({ // HTTP method (default POST) method: 'POST',
// Handler function handler(req, res) { const body = req.body as CreateUserBody // Handle request... res.status(201).json({ id: 'new-user-id' }) }})Supported HTTP Methods
Section titled “Supported HTTP Methods”type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS'HttpRequest Object
Section titled “HttpRequest Object”The HTTP request object contains the following properties:
interface HttpRequest { /** Raw Node.js IncomingMessage */ raw: IncomingMessage
/** HTTP method */ method: string
/** Request path */ path: string
/** Route parameters (extracted from URL path, e.g., /users/:id) */ params: Record<string, string>
/** Query parameters */ query: Record<string, string>
/** Request headers */ headers: Record<string, string | string[] | undefined>
/** Parsed request body */ body: unknown
/** Client IP */ ip: string}Usage Examples
Section titled “Usage Examples”export default defineHttp({ method: 'GET', handler(req, res) { // Get query parameters const page = parseInt(req.query.page ?? '1') const limit = parseInt(req.query.limit ?? '10')
// Get request headers const authHeader = req.headers.authorization
// Get client IP console.log('Request from:', req.ip)
res.json({ page, limit }) }})Body Parsing
Section titled “Body Parsing”The request body is automatically parsed based on Content-Type:
application/json- Parsed as JSON objectapplication/x-www-form-urlencoded- Parsed as key-value object- Others - Kept as raw string
export default defineHttp<{ name: string; age: number }>({ method: 'POST', handler(req, res) { // body is already parsed const { name, age } = req.body as { name: string; age: number } res.json({ received: { name, age } }) }})HttpResponse Object
Section titled “HttpResponse Object”The HTTP response object provides a chainable API:
interface HttpResponse { /** Raw Node.js ServerResponse */ raw: ServerResponse
/** Set status code */ status(code: number): HttpResponse
/** Set response header */ header(name: string, value: string): HttpResponse
/** Send JSON response */ json(data: unknown): void
/** Send text response */ text(data: string): void
/** Send error response */ error(code: number, message: string): void}Usage Examples
Section titled “Usage Examples”export default defineHttp({ method: 'POST', handler(req, res) { // Set status code and custom headers res .status(201) .header('X-Custom-Header', 'value') .json({ created: true }) }})export default defineHttp({ method: 'GET', handler(req, res) { // Send error response res.error(404, 'Resource not found') // Equivalent to: res.status(404).json({ error: 'Resource not found' }) }})export default defineHttp({ method: 'GET', handler(req, res) { // Send plain text res.text('Hello, World!') }})File Routing Conventions
Section titled “File Routing Conventions”Name Conversion
Section titled “Name Conversion”File names are automatically converted to route paths:
| File Path | Route Path (prefix=/api) |
|---|---|
login.ts | /api/login |
users/profile.ts | /api/users/profile |
users/[id].ts | /api/users/:id |
game/room/[roomId].ts | /api/game/room/:roomId |
Dynamic Route Parameters
Section titled “Dynamic Route Parameters”Use [param] syntax to define dynamic parameters:
import { defineHttp } from '@esengine/server'
export default defineHttp({ method: 'GET', handler(req, res) { // Get route parameter directly from params const { id } = req.params res.json({ userId: id }) }})Multiple parameters:
import { defineHttp } from '@esengine/server'
export default defineHttp({ method: 'GET', handler(req, res) { const { userId, postId } = req.params res.json({ userId, postId }) }})Skip Rules
Section titled “Skip Rules”The following files are automatically skipped:
- Files starting with
_(e.g.,_helper.ts) index.ts/index.jsfiles- Non
.ts/.js/.mts/.mjsfiles
Directory Structure Example
Section titled “Directory Structure Example”src/└── http/ ├── _utils.ts # Skipped (underscore prefix) ├── index.ts # Skipped (index file) ├── health.ts # GET /api/health ├── login.ts # POST /api/login ├── register.ts # POST /api/register └── users/ ├── index.ts # Skipped ├── list.ts # GET /api/users/list └── [id].ts # GET /api/users/:idCORS Configuration
Section titled “CORS Configuration”Quick Enable
Section titled “Quick Enable”const server = await createServer({ port: 3000, cors: true // Use default configuration})Custom Configuration
Section titled “Custom Configuration”const server = await createServer({ port: 3000, cors: { // Allowed origins origin: ['http://localhost:5173', 'https://myapp.com'], // Or use wildcard // origin: '*', // origin: true, // Reflect request origin
// Allowed HTTP methods methods: ['GET', 'POST', 'PUT', 'DELETE'],
// Allowed request headers allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
// Allow credentials (cookies) credentials: true,
// Preflight cache max age (seconds) maxAge: 86400 }})CorsOptions Type
Section titled “CorsOptions Type”interface CorsOptions { /** Allowed origins: string, string array, true (reflect) or '*' */ origin?: string | string[] | boolean
/** Allowed HTTP methods */ methods?: string[]
/** Allowed request headers */ allowedHeaders?: string[]
/** Allow credentials */ credentials?: boolean
/** Preflight cache max age (seconds) */ maxAge?: number}Route Merging
Section titled “Route Merging”File routes and inline routes can be used together, with inline routes having higher priority:
const server = await createServer({ port: 3000, httpDir: './src/http', httpPrefix: '/api',
// Inline routes merge with file routes http: { '/health': (req, res) => res.json({ status: 'ok' }), '/api/special': (req, res) => res.json({ special: true }) }})Sharing Port with WebSocket
Section titled “Sharing Port with WebSocket”HTTP routes automatically share the same port with WebSocket services:
const server = await createServer({ port: 3000, // WebSocket related config apiDir: './src/api', msgDir: './src/msg',
// HTTP related config httpDir: './src/http', httpPrefix: '/api', cors: true})
await server.start()
// Same port 3000:// - WebSocket: ws://localhost:3000// - HTTP API: http://localhost:3000/api/*Complete Examples
Section titled “Complete Examples”Game Server Login API
Section titled “Game Server Login API”import { defineHttp } from '@esengine/server'import { createJwtAuthProvider } from '@esengine/server/auth'
interface LoginRequest { username: string password: string}
interface LoginResponse { token: string userId: string expiresAt: number}
const jwtProvider = createJwtAuthProvider({ secret: process.env.JWT_SECRET!, expiresIn: 3600})
export default defineHttp<LoginRequest>({ method: 'POST', async handler(req, res) { const { username, password } = req.body as LoginRequest
// Validate user const user = await db.users.findByUsername(username) if (!user || !await verifyPassword(password, user.passwordHash)) { res.error(401, 'Invalid username or password') return }
// Generate JWT const token = jwtProvider.sign({ sub: user.id, name: user.username, roles: user.roles })
const response: LoginResponse = { token, userId: user.id, expiresAt: Date.now() + 3600 * 1000 }
res.json(response) }})Game Data Query API
Section titled “Game Data Query API”import { defineHttp } from '@esengine/server'
export default defineHttp({ method: 'GET', async handler(req, res) { const limit = parseInt(req.query.limit ?? '10') const offset = parseInt(req.query.offset ?? '0')
const players = await db.players.findMany({ sort: { score: 'desc' }, limit, offset })
res.json({ data: players, pagination: { limit, offset } }) }})Middleware
Section titled “Middleware”Middleware Type
Section titled “Middleware Type”Middleware are functions that execute before and after route handlers:
type HttpMiddleware = ( req: HttpRequest, res: HttpResponse, next: () => Promise<void>) => void | Promise<void>Built-in Middleware
Section titled “Built-in Middleware”import { requestLogger, bodyLimit, responseTime, requestId, securityHeaders} from '@esengine/server'
const server = await createServer({ port: 3000, http: { /* ... */ }, // Global middleware configured via createHttpRouter})requestLogger - Request Logging
Section titled “requestLogger - Request Logging”import { requestLogger } from '@esengine/server'
// Log request and response timerequestLogger()
// Also log request bodyrequestLogger({ logBody: true })bodyLimit - Request Body Size Limit
Section titled “bodyLimit - Request Body Size Limit”import { bodyLimit } from '@esengine/server'
// Limit request body to 1MBbodyLimit(1024 * 1024)responseTime - Response Time Header
Section titled “responseTime - Response Time Header”import { responseTime } from '@esengine/server'
// Automatically add X-Response-Time headerresponseTime()requestId - Request ID
Section titled “requestId - Request ID”import { requestId } from '@esengine/server'
// Auto-generate and add X-Request-ID headerrequestId()
// Custom header namerequestId('X-Trace-ID')securityHeaders - Security Headers
Section titled “securityHeaders - Security Headers”import { securityHeaders } from '@esengine/server'
// Add common security response headerssecurityHeaders()
// Custom configurationsecurityHeaders({ hidePoweredBy: true, frameOptions: 'DENY', noSniff: true})Custom Middleware
Section titled “Custom Middleware”import type { HttpMiddleware } from '@esengine/server'
// Authentication middlewareconst authMiddleware: HttpMiddleware = async (req, res, next) => { const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) { res.error(401, 'Unauthorized') return // Don't call next(), terminate request }
// Validate token... (req as any).userId = 'decoded-user-id'
await next() // Continue to next middleware and handler}Using Middleware
Section titled “Using Middleware”With createHttpRouter
Section titled “With createHttpRouter”import { createHttpRouter, requestLogger, bodyLimit } from '@esengine/server'
const router = createHttpRouter({ '/api/users': (req, res) => res.json([]), '/api/admin': { GET: { handler: (req, res) => res.json({ admin: true }), middlewares: [adminAuthMiddleware] // Route-level middleware } }}, { middlewares: [requestLogger(), bodyLimit(1024 * 1024)], // Global middleware timeout: 30000 // Global timeout 30 seconds})Request Timeout
Section titled “Request Timeout”Global Timeout
Section titled “Global Timeout”import { createHttpRouter } from '@esengine/server'
const router = createHttpRouter({ '/api/data': async (req, res) => { // If processing exceeds 30 seconds, auto-return 408 Request Timeout await someSlowOperation() res.json({ data: 'result' }) }}, { timeout: 30000 // 30 seconds})Route-level Timeout
Section titled “Route-level Timeout”const router = createHttpRouter({ '/api/quick': (req, res) => res.json({ fast: true }),
'/api/slow': { POST: { handler: async (req, res) => { await verySlowOperation() res.json({ done: true }) }, timeout: 120000 // This route allows 2 minutes } }}, { timeout: 10000 // Global 10 seconds (overridden by route-level)})Best Practices
Section titled “Best Practices”- Use defineHttp - Get better type hints and code organization
- Unified Error Handling - Use
res.error()to return consistent error format - Enable CORS - Required for frontend-backend separation
- Directory Organization - Organize HTTP route files by functional modules
- Validate Input - Always validate
req.bodyandreq.querycontent - Status Code Standards - Follow HTTP status code conventions (200, 201, 400, 401, 404, 500, etc.)
- Use Middleware - Implement cross-cutting concerns like auth, logging, rate limiting via middleware
- Set Timeouts - Prevent slow requests from blocking the server