railiz
v0.5.0
Published
Lightweight Node.js engine for deterministic, intent-driven domain logic orchestration.
Maintainers
Keywords
Readme
🌐 railiz
🚀 A deterministic, high-performance HTTP engine for modern TypeScript backends.
It’s not a full framework; it’s the engine
Railiz is the runtime layer for building your own backend architecture.
Basic
import { Railiz } from 'railiz'
const app = new Railiz()
app.get('/', (ctx) => {
ctx.json({ hello: 'railiz' })
})
app.run(3000)What is Railiz (in 10 seconds)
- Not a framework → no opinions forced
- Not a router → more than routing
- Not just middleware → execution engine
👉 Railiz = control + guarantees
Why railiz?
Most Node frameworks make you choose:
- Flexibility → chaos (Express)
- Speed → constraints (Fastify)
- Structure → heaviness (NestJS)
railiz removes the trade-off.
Core Features
- ⚡ Radix-tree routing (O(k))
- 🧠 Deterministic middleware execution (no order bugs)
- 🧩 Pipeline-based orchestration (not just chain)
- 🔌 Plugin-first architecture
- 🪶 Ultra-lightweight core
- 📝 Full TypeScript inference (params, context)
- 🌐 Framework-agnostic (works anywhere)
Build with railiz?
- REST APIs (faster & safer than Express)
- Custom backend frameworks (like NestJS, but yours)
- High-performance microservices
- Edge / serverless handlers
Mental Model
Request
↓
Context
↓
Pipeline (middlewares)
↓
Router (linear | hybrid | radix | compiled)
↓
Handler
↓
Response👉 Everything is explicit.
👉 Nothing is hidden.
Installation
npm install railizQuick example
import { Railiz, safeHandler } from 'railiz'
const app = new Railiz()
// Execution order is GUARANTEED
app.pipeline((p) => {
const auth = p.use(authMiddleware)
p.use(rateLimit).after(auth)
})
app.get('/', safeHandler(async (ctx) => {
return {
message: 'Hello Railiz 🚀',
url: ctx.url,
host: ctx.host,
ip: ctx.ip,
headers: ctx.headers,
}
}))
app.get('/page404', (ctx) => {
ctx.throw(404, 'Page not found')
})
app.createServer().listen(3000, () => {
console.log('🚀 Server running at http://localhost:3000')
})
// app.run(3000)Deterministic Execution
No more middleware order bugs.
app.pipeline((p) => {
const auth = p.use(authMiddleware)
const db = p.use(dbMiddleware)
p.use(cache).before(db)
p.use(rateLimit).after(auth)
})auth → rateLimit → cache → db👉 Execution order is guaranteed.
❌ No middleware order bugs
❌ No “who runs first?” confusion.
Context API
| Property / Method | Type / Usage | Description |
| -------------------------------------------- | ------------------------ | -------------------------------------------------------------- |
| ctx.params | Record<string, string> | URL parameters, e.g. /users/:id → { id: '123' } |
| ctx.query | Record<string, string> | Query string params, e.g. /search?q=hello → { q: 'hello' } |
| ctx.data.body | any | Parsed request body (JSON/form) |
| ctx.state | any | Per-request storage for middleware or handler |
| ctx.json<T>(data: T) | Function | Send JSON response |
| ctx.text(text: string) | Function | Send plain text |
| ctx.html(html: string) | Function | Send HTML response |
| ctx.send(data: any) | Function | Generic send (auto-detect type) |
| ctx.statusCode(code: number) | Function | Set HTTP status code |
| ctx.set(name: string, value: string) | Function | Set header |
| ctx.redirect(url: string, status?: number) | Function | Redirect client |
| ctx.sendFile(filePath: string) | Function | Send static file |
| ctx.ok<T>(data?: T) | Function | Shortcut 200 OK (JSON) |
| ctx.created<T>(data?: T) | Function | Shortcut 201 Created |
| ctx.accepted<T>(data?: T) | Function | Shortcut 202 Accepted |
| ctx.noContent() | Function | Shortcut 204 No Content |
| ctx.badRequest(msg?: string) | Function | Shortcut 400 Bad Request |
| ctx.unauthorized(msg?: string) | Function | Shortcut 401 Unauthorized |
| ctx.forbidden(msg?: string) | Function | Shortcut 403 Forbidden |
| ctx.notFound(msg?: string) | Function | Shortcut 404 Not Found |
| ctx.internalError(msg?: string) | Function | Shortcut 500 Internal Server Error |
👉 Inspired by Koa, but stricter and typed.
⚠️ TTL and expiration values are always in milliseconds (ms) Example:
60000 = 60 seconds
Routing
Railiz uses a multi-stage routing engine that automatically adapts based on the number of registered routes.
| Mode | Engine | When used | |----------|-------------------------|-----------------| | linear | simple sequential scan | < 100 routes | | hybrid | indexed + LRU cache | < 1000 routes | | compiled | precompiled matcher | > 1000 routes |
Routing is designed to be explicit, predictable, and REST-friendly. Wildcards (*) are only allowed for middleware, fallback, or proxy routes, and should be avoided in core API endpoints to prevent ambiguity.
Router Options
const app = new Railiz() // auto mode (recommended)
Or explicitly:
const app = new Railiz({ router: 'linear' })
Available modes:
- linear → simple matcher (small apps, fast startup)
- radix → radix tree routing (default fast mode)
- auto → automatic engine switching based on scale
- compiled → precompiled route matcher (large-scale production)
Supported Routes
| Type | Path | Example |
|--------|--------------|----------------------------------|
| Static | /users | List all users |
| Param | /users/:id | Get user by ID |
Route Definition
const app = new Railiz()
app.route({ method: 'GET', path: '/users/:id', middleware: [auth], handler: async (ctx) => { return ctx.ok({ id: ctx.params.id }) }, })
Routing Evolution
Railiz automatically upgrades its routing engine as the application scales:
- < 100 routes → linear scan (minimal overhead, fastest startup)
- < 1000 routes → hybrid engine (LRU + indexed lookup)
1000 routes → compiled router (precomputed match functions for maximum throughput)
Grouping
basic
app.group('/api', (r) => {
r.get('/health', ctx => ctx.ok())
})with middleware
app.group('/admin', [authMiddleware], (r) => {
r.get('/dashboard', (ctx) => ctx.ok({ message: 'Admin dashboard' }))
})nested
app.group('/api', (r) => {
r.group('/v1', (r2) => {
r2.get('/status', (ctx) => ctx.ok({ status: 'ok' }))
})
})Plugin System
app.plugin((app) => {
app.use(async (ctx, next) => {
console.log('Custom plugin triggered', ctx.path)
await next?.()
})
})You can create your own plugins for purposes such as:
- Proxy requests: forward requests to another server (reverse proxy)
- Auth / JWT: add middleware to check tokens
💡 Notes:
- Pipeline order matters: Plugins are executed in the order they are registered. Place app.plugin calls carefully to control execution sequence.
- Reusability: Plugins can be packaged and reused across multiple apps, making it easy to share common functionality like logging, auth, or proxying.
Middleware
app.use(async (ctx, next) => {
console.log(ctx.path)
await next()
})Error Boundary
app.route({
method: 'GET',
path: '/boom',
handler: async () => {
throw new Error('boom')
},
errorBoundary: async (ctx) => {
ctx.status = 500
ctx.body = 'Handled error'
},
})Http Error
| Case | Use | | ---------------- | -------------------- | | 4xx client error | ✅ HttpError | | 5xx system error | ❌ normal Error | | validation | ✅ HttpError(400) | | auth | ✅ HttpError(401/403) |
if (!ctx.user) {
throw new HttpError(401, 'Unauthorized')
}throw new HttpError(422, 'Validation failed', {
field: 'email',
reason: 'invalid format',
})Presets
Built-in Presets:
- apiPreset – JSON parsing, query parser, optional CORS.
- loggerPreset – Log request → response time.
- authPreset – JWT / API key auth middleware.
Example with Presets & Dependencies:
import { Railiz } from 'railiz'
import { apiPreset, loggerPreset, authPreset, applyPresets } from 'railiz'
const app = new Railiz({ trace: true })
const presets = [
apiPreset({ cors: true }),
loggerPreset({ enabled: true }),
authPreset({ strategy: 'jwt' }),
]
// Example: authPreset depends on apiPreset
presets.find(p => p.name === 'auth').dependsOn = ['api']
applyPresets(app, presets)
app.get('/profile', (ctx) => {
ctx.ok({ user: ctx.state.user })
})
app.run(3000)Hooks
app.on('request', (ctx) => console.log('Incoming request', ctx.path))
app.on('response', (ctx) => console.log('Response status', ctx.status))
app.on('error', (ctx, err) => console.error('Error:', err.message))App Factory (createApp)
const { app } = createApp({
// railizOptions: {
// router: 'radix',
// },
presets: [
loggerPreset({ enabled: true }),
],
debug: true,
onCreate() {
console.log('🚀 create')
},
onReady() {
console.log('✅ ready')
},
})
app.get('/hello', (ctx) => {
return ctx.ok({ message: 'Hello Railiz' })
})
app.run(3000)
// NODE_ENV != production => 🚀 Server running at http://localhost:3000Build your own opinionated backend in seconds.
Background Tasks
Railiz allows running tasks after the response has been sent:
app.get('/send-email', async (ctx) => {
// Respond immediately
ctx.ok({ message: 'Email will be sent in background' })
// Push background task
ctx.backgroundTasks!.push(async () => {
await sendEmail(ctx.query.to, 'Hello from Railiz!')
console.log(`✅ Email sent to ${ctx.query.to}`)
})
})- ctx.backgroundTasks is a per-request array.
- Tasks run in parallel, and errors are logged without affecting the response.
- You can push multiple tasks at once:
ctx.backgroundTasks!.push(task1, task2, task3)DI (Dependency Injection)
Register DI
app.inject('db', () => new Database(), 'singleton')
app.inject('requestId', () => crypto.randomUUID(), 'scoped')
app.inject('cache', () => new Map(), 'transient')
app.get('/test', (ctx) => {
const db = ctx.di.resolve('db')
const user = await db.user.findMany()
const requestId = ctx.di.resolve('requestId')
return ctx.json({ requestId })
})Scoped DI behavior
| Scope | Behavior | | --------- | -------------------- | | singleton | 1 instance app | | scoped | 1 instance / request | | transient | always create |
scoped = per request context
Adapter Support (Quick)
- Express (expressAdapter)
- Fastify (fastifyAdapter)
- Lambda (lambdaAdapter)
- Cloudflare Workers (workersAdapter)
- Bun (bunAdapter)
app.use(expressAdapter(app))
export const handler = lambdaAdapter(app)
export default workersAdapter(app)
Bun.serve(bunAdapter(app))
fastify.all('*', fastifyAdapter(app))
Ecosystem (Middleware Runtime)
app.use(errorHandler())| Middleware / Feature | Node.js | Lambda | Edge | Notes |
|------------------------|:------: |:------:|:-----:|----------------------------------------------------|
| serveStatic | ❌ | ❌ | ❌ | Static assets (early exit before pipeline) |
| logger | ✅ | ✅ | ✅ | Logs request lifecycle |
| normalizeHeaders | ⚠️ | ⚠️ | ⚠️ | Normalizes incoming headers |
| cors | ✅ | ✅ | ✅ | CORS policy |
| helmet | ✅ | ✅ | ⚠️ | Security headers |
| json parser | 🔁 | 🔁 | ⚠️ | Parses JSON body |
| bodyParser | 🔁 | ❌ | ❌ | Parses form/urlencoded bodies |
| queryParser | ✅ | ✅ | ✅ | Parses query string |
| jwtAuth | ✅ | ✅ | ⚠️ | Authentication layer |
| validateDynamic | ✅ | ✅ | ✅ | Request validation (headers/body/transform) |
| rateLimit | ❌ | ❌ | ❌ | Traffic control (anti-abuse protection) |
| timeout | ❌ | ❌ | ❌ | Aborts slow requests |
| cache (memory) | ❌ | ❌ | ❌ | Local cache (not distributed-safe) |
| httpResponseCache | ❌ | ❌ | ❌ | Full HTTP response caching |
| session | ⚠️ | ❌ | ❌ | Stateful session storage |
| cookies | ⚠️ | ⚠️ | ⚠️ | Cookie handling |
| retry | ⚠️ | ⚠️ | ⚠️ | Retry transient failures |
| redirect | ⚠️ | ⚠️ | ⚠️ | Redirect handling |
| buffer | ❌ | ❌ | ❌ | Response buffering layer |
| circuit-breaker | ⚠️ | ⚠️ | ⚠️ | Prevents cascading failures |
| dedupe | ⚠️ | ⚠️ | ⚠️ | Single-flight request deduplication |
| content-negotiation | ✅ | ✅ | ⚠️ | Response format negotiation |
| metrics | ⚠️ | ⚠️ | ⚠️ | Observability (latency, errors, counts) |
| errorHandler | ✅ | ✅ | ✅ | Global error boundary (MUST be last) |
✅ Fully supported.
⚠️ Works with limitations.
🔁 Requires hybrid/adapter implementation.
❌ Not suitable for runtime.
Middleware should be applied in order from inbound (security + parsing) → authentication + validation → traffic control → cache + business logic → resilience → response → error handler at the end.
Example
import {
Railiz,
json,
bodyParser,
logger,
cors,
rateLimit,
cache,
cookies,
httpResponseCache,
helmet,
validateDynamic,
timeout,
retry,
redirect,
buffer,
errorHandler,
serveStatic
} from 'railiz'
const app = new Railiz()
app.use(serveStatic('./public'))
// serves static files (HTML, CSS, JS) with early exit
app.use(logger())
// logs incoming requests + responses
app.use(cors({ origin: '*' }))
// enables cross-origin requests
app.use(helmet())
// adds security headers (XSS, clickjacking protection)
// --------------------
// Parsing layer
// --------------------
app.use(json())
// parses JSON request body
app.use(bodyParser({ json: { limit: '2mb' } }))
// parses form/urlencoded + enforces body size limit
// --------------------
// Auth layer (MUST be here)
// --------------------
app.use(jwtAuth('mysecret', { mandatory: true }))
// verifies JWT and blocks unauthorized requests
// --------------------
// Traffic control
// --------------------
app.use(rateLimit({ limit: 100, windowMs: 60_000 }))
// prevents abuse by limiting requests per time window
app.use(timeout({ response: 5000, socket: 10000 }))
// aborts slow/hanging requests
// --------------------
// Cache layer
// --------------------
app.use(cache({ ttl: 30_000 }))
// generic cache for data/service-level results
app.use(httpResponseCache({ ttl: 60 }))
// caches full HTTP responses per request key
// --------------------
// Reliability layer
// --------------------
app.use(retry({ limit: 2, interval: 100 }))
// retries failed transient operations
app.use(redirect({ limit: 3, sameHost: true }))
// handles redirects safely with limits
// --------------------
// Response processing
// --------------------
app.use(cookies({ ttl: 60 * 60 * 1000 }))
// manages cookie storage and sending
app.use(buffer())
// buffers response for post-processing
// --------------------
// Error boundary (ALWAYS LAST)
// --------------------
app.use(errorHandler())
// catches all errors and prevents crash
// ------------------------
// Routes
// ------------------------
// Simple GET
app.get('/ping', (ctx) => ctx.ok({ message: 'pong' }))
// POST with validation
app.post(
'/users',
validateDynamic({
body: (data) => {
if (!data.name) return 'Missing name'
if (!data.age || typeof data.age !== 'number') return 'Invalid age'
return true
},
}),
(ctx) => {
if (ctx.response.validate?.body) {
return ctx.badRequest(ctx.response.validate.body)
}
const id = Date.now()
return ctx.created({ id, ...ctx.data.body })
}
)
// Route with redirect
app.get('/old-route', (ctx) => ctx.redirect('/new-route'))
app.get('/new-route', (ctx) => ctx.ok({ message: 'You are redirected here!' }))
// POST that sets cookies
app.post('/login', (ctx) => {
const { username } = ctx.data.body || {}
if (!username) return ctx.badRequest('Missing username')
ctx.response!.headers = ctx.response!.headers || {}
ctx.response!.headers['Set-Cookie'] = [`user=${username}; HttpOnly; Max-Age=3600`]
return ctx.ok({ message: `Welcome ${username}` })
})
// GET route that reads cookies
app.get('/me', (ctx) => {
const cookies = ctx.req.headers['cookie'] || ''
return ctx.ok({ cookies })
})
// Fallback route
app.get('/*', (ctx) => ctx.notFound('Route not found'))
// ------------------------
// Run server
// ------------------------
app.run(3000)
console.log('🚀 Railiz server running on http://localhost:3000')security → parse → auth → rate limit → cache → business → response → error
OpenAPI
This plugin automatically generates OpenAPI 3 documentation for your Railiz app and can optionally mock API responses for rapid development, limitation (type inference partial / runtime metadata required)
Features
- Automatically collects metadata from your routes.
- Supports path parameters, query parameters, and request/response bodies.
- Exposes /openapi.json endpoint for OpenAPI 3 spec.
- Can mock API responses based on defined schemas.
- Works with both linear and radix routers.
Use
import { Railiz, openApi } from 'railiz'
const app = new Railiz({ router: 'linear' })
// Register plugin with mock responses
app.plugin(openApi({ title: 'My API', version: '1.0.0', mock: true }))
// Define routes
app.get('/users/:id', async (ctx) => {
ctx.json({ success: true, userId: ctx.params.id })
})
app.post('/login', async (ctx) => {
ctx.json({ success: true, token: 'fake-jwt-token' })
})
// Run server
app.run(3000)Accessing OpenAPI
Visit http://localhost:3000/openapi.json to see your API documentation.Mock Responses
When mock: true is enabled, routes return fake data based on their response schema, allowing frontend teams to start development before the backend is fully implemented.
Cache System
Railiz provides a 3-layer caching model:
Cache Architecture
L1 (memo) → L2 (requestCache) → Plugin Cache → Origin (DB/API)
Priority
When cache layers are combined, resolution order is:
- DI cacheClient (HIGHEST)
- L1 memo()
- L2 requestCache()
- Plugin cache
L1 memo() (In-request)
Use case:
- deduplicate repeated calls in same request
- avoid redundant DB/API calls
app.get('/users/:id', async (ctx) => {
const user = await ctx.memo(
{ id: ctx.params.id },
() =>
ctx.requestCache(
{ route: 'user', id: ctx.params.id },
() => getUser(ctx.params.id),
{ ttl: 30_000 },
),
{ ttl: 5_000 },
)
return ctx.json({
cache: ctx.state.cacheHit ?? 'MISS',
user,
})
})Use case:
- deduplicate repeated calls in same request
- avoid redundant DB/API calls
L2 requestCache()
Use case:
- cache API responses
- reduce DB load
- shared across requests
app.get('/users/:id', async (ctx) => {
const user = await ctx.requestCache(
{ route: 'user', id: ctx.params.id },
() => getUser(ctx.params.id),
{
ttl: 30000, debug: true,
// force: true,
// FORCE REFRESH (bypass cache completely)
}
)
return ctx.json({
source: 'L2 requestCache',
cache: ctx.state.cacheHit ?? 'MISS',
user,
})
})DI Override
app.inject('cacheClient', new MemoryCache())Plugin Cache
app.plugin(cachePlugin([new MemoryCache()]))inject > plugin
Circuit Breaker
Protect your system from cascading failures by short-circuiting unstable services.
Basic usage
import { circuitBreaker } from 'railiz'
app.use(
circuitBreaker({
failureThreshold: 5,
resetTimeout: 30_000, // ms
})
)Per-service isolation (recommended)
app.use(
circuitBreaker({
key: (ctx) => {
if (ctx.path.startsWith('/payments')) return 'payment-service'
if (ctx.path.startsWith('/users')) return 'user-service'
return 'default'
},
})
)⚠️ IMPORTANT:
If you don't provide a
key, the circuit state is GLOBAL. One failing route can block all others. Usekeyto isolate circuits per service or dependency.
Each service has its own circuit state
Example with external API
app.get(
'/payments/:id',
circuitBreaker({
key: () => 'payment-api',
failureThreshold: 3,
resetTimeout: 10_000,
}),
async (ctx) => {
const data = await fetchPayment(ctx.params.id) // external call
ctx.ok(data)
}
)Behavior
| State | Description | | --------- | --------------------------------- | | CLOSED | Normal operation | | OPEN | Requests are blocked (fast fail) | | HALF_OPEN | Allows 1 request to test recovery |
Flow
CLOSED --(fail x N)--> OPEN
OPEN --(after resetTimeout)--> HALF_OPEN
HALF_OPEN --(success)--> CLOSED
HALF_OPEN --(fail)--> OPENWhen circuit is OPEN
// HTTP Status: 503
{
"message": "Service unavailable (circuit open: payment-api)"
}Use cases
- External APIs (payment, auth, analytics)
- Microservices communication
- Database protection (when unstable)
Tip
// ❌ BAD (global breaker)
app.use(circuitBreaker())
// ✅ GOOD (isolated per dependency)
app.use(
circuitBreaker({
key: (ctx) => ctx.path.startsWith('/payments')
? 'payment-api'
: 'default'
})
)Always prefer per dependency, not global
Architecture
Node HTTP
↓
Railiz Core
↓
Router (Radix / Linear)
↓
Context
↓
HandlersComparison
railiz is the only one here that gives you deterministic middleware execution.
| Criteria | Railiz | Express.js | Fastify | NestJS | | ------------------- | --------------------- | ------------------- | ----------------- | ---------------------- | | Core concept | Execution engine | Minimal framework | Web framework | Full framework | | Abstraction level | 🔥 Low (full control) | Low | Medium | High | | Middleware model | ✅ deterministic | ❌ implicit order | ⚠️ plugin-based | ⚠️ decorator-based | | Routing performance | ⚡ Radix O(k) | ❌ Linear scan | ⚡ Optimized | ⚡ (Fastify under hood) | | Type safety | ✅ strong | ❌ weak | ✅ strong | ✅ strong | | Architecture | ✅ flexible core | ❌ unstructured | ⚠️ opinionated | ⚠️ enforced patterns | | Plugin system | 🔌 simple & explicit | ⚠️ ad-hoc | ✅ rich | ✅ DI-based | | Boilerplate | 🪶 minimal | 🪶 minimal | ⚠️ medium | ❌ high | | Learning curve | 🟢 low | 🟢 low | 🟡 medium | 🔴 high | | Use case | Engine / custom arch | Small apps / legacy | APIs / services | Enterprise apps |
💡 Takeaways:
- Express → Freedom, then chaos.
- Fastify → Structured, plugin-based.
- NestJS → Enterprise-ready, heavy, opinionated.
- Railiz → Lightweight, deterministic, full control — build your own backend architecture with guarantees.
Design Principles
- Deterministic execution > implicit magic
- Explicit routing > dynamic guessing
- Runtime control > framework opinion
- Composition over inheritance
When to Use
- Build your own backend framework
- Internal APIs
- High-performance systems
- Edge runtimes
Performance
- Radix routing: O(k)
- ~2-5x faster than Express (micro benchmarks)
When NOT to use Railiz
- You want a batteries-included framework → use NestJS
- You don’t care about execution order → use Express
- You want conventions over control → use Fastify
👉 Railiz is for engineers who want control.
Philosophy
You control:
- architecture
- data
- plugins
railiz controls:
- execution
- routing
- lifecycleLicense
MIT
