railiz-http
v2.0.0
Published
Lightweight HTTP engine built on Railiz for building modular, type-safe, and deterministic backend APIs with plugin-based architecture.
Maintainers
Keywords
Readme
🌐 railiz-http
🚀 A smart HTTP client layer built for Railiz ecosystem
Built for control, caching, retries, and resilience
Designed for distributed backend systems
Why railiz-http?
- ⚡ Global + per-request config merging
- 🧠 L1 / L2 caching (memo + requestCache)
- 🔁 Retry with exponential backoff
- 🛡 Circuit breaker support
- ⏱ Timeout control
- 🔌 Middleware pipeline (like Koa, but typed)
- 🌍 BaseURL support
- 🧩 Fully compatible with
Railiz Context
Installation
npm install railiz-http railiz-resilience railizQuick example
import { useHttp } from 'railiz-http'
app.get('/users/:id', async (ctx) => {
const http = useHttp({
context: ctx,
options: {
baseURL: 'https://jsonplaceholder.typicode.com',
},
})
const user = await http.get(`/users/${ctx.params.id}`)
return ctx.json({ user })
})Global Config
You can define global HTTP behavior once:
import { createHttpContext, setGlobalHttpConfig, useHttp } from 'railiz-http'
setGlobalHttpConfig({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 2000,
retry: {
attempts: 3,
delay: 300,
},
cache: {
ttl: 5000,
swr: 10000,
},
})Context Model
Standalone Context
const ctx = createHttpContext()
// const ctx = createHttpContext(your-context)
const http = useHttp({ context: ctx, options: { timeout: 3000 } })Railiz Context
app.get('/memo-demo', async (ctx) => {
const http = useHttp({ context: ctx })
})Global config is read-only and only used as a base for per-request configuration.
Caching System
L1 + L2 Cache
| Layer | Type | Scope | |-------|-------------|--------------| | L1 | memo | per request | | L2 | requestCache| shared cache |
Memo (L1 Cache - Per Request)
app.get('/memo-demo', async (ctx) => {
const http = useHttp({
context: ctx,
options: {
baseURL: 'https://jsonplaceholder.typicode.com',
},
})
const user1 = await http.get('/users/1', {
memo: true,
})
const user2 = await http.get('/users/1', {
memo: true,
})
return ctx.json({
memoHit: user1 === user2, // true (same reference)
user1,
// user2, // "user2": "[Circular]",
user2Copy: JSON.parse(JSON.stringify(user2)) // same json
})
})Memo with TTL
const user = await http.get('/users/1', {
memo: {
ttl: 5000,
debug: true,
},
})L2 Cache (shared cache)
const user = await http.get('/users/1', {
cache: {
ttl: 10000,
swr: 15000,
key: 'user:1',
tags: ['users'],
},
})Memo + Cache hybrid
app.get('/users/:id', async (ctx) => {
const http = useHttp({
context: ctx,
options: {
baseURL: 'https://jsonplaceholder.typicode.com',
},
})
const user = await http.get(`/users/${ctx.params.id}`, {
// L1: memo (per-request dedupe)
memo: {
ttl: 5000,
debug: true,
},
// L2: shared cache (cross-request)
cache: {
ttl: 10000,
swr: 20000,
key: `user:${ctx.params.id}`,
tags: ['users'],
},
})
return ctx.json({ user })
})Compare: Memo + Cache
| Feature | Memo (L1) | Cache (L2) | |-----------|---------------|--------------| | Scope | per request | shared | | Lifetime | ctx lifecycle | TTL based | | Purpose | dedupe calls | persist data |
Memo is NOT persistence. It only deduplicates in the same request lifecycle.
Key Insight
memo→ prevents duplicate calls within the same requestcache→ persists data across requests- Using both together → optimal performance + consistency
Retry
await http.get('/unstable-api', {
retry: {
attempts: 3,
delay: 200,
},
})Behavior
attempt 1 → fail
attempt 2 → retry (delay 200ms)
attempt 3 → retry (400ms)Circuit Breaker
Protect external APIs from cascading failure.
Basic usage
const data = await http.get('/payments', {
breaker: {
key: 'payment-service',
},
})Full production config
app.get(
'/checkout',
safeHandler(async (ctx) => {
const http = useHttp({
context: ctx,
options: {
baseURL: 'https://api.payment.com',
timeout: 3000,
},
})
const payment = await http.post(
'/charge',
{
amount: 100,
currency: 'USD',
},
{
// ❌ NEVER cache payment
cache: false,
memo: false,
// 🔁 retry (network / transient error)
retry: {
attempts: 2,
delay: 200,
},
// 🛡 circuit breaker: protect downstream
breaker: {
key: 'payment-service',
failureThreshold: 3,
resetTimeout: 15000,
},
},
)
return {
success: true,
payment,
}
}),
)Real production pattern (microservices)
const user = await http.get('/users/1', {
// 🛡 protect downstream service
breaker: {
key: 'user-service',
failureThreshold: 5,
resetTimeout: 10000,
},
// 🔁 retry: transient error
retry: {
attempts: 2,
delay: 200,
},
// 🧠 L1 memo (dedupe request)
memo: true,
// 💾 L2 cache (cross-request)
cache: {
key: 'user:1', // ✅ explicit
ttl: 10000,
swr: 20000,
tags: ['users'], // ✅ for invalidate
},
})Note: advanced breaker options depend on railiz-resilience configuration.
Timeout
await http.get('/slow-api', {
timeout: 1500,
})If request exceeds timeout → aborted automatically.
Middleware
http.use(async (ctx, next) => {
console.log('Request:', ctx.request?.url)
return next()
})Unlike Railiz middleware (server-level), this runs at the HTTP client layer — intercepting outbound requests (fetch calls).
Railiz App
import {
Railiz,
bodyParser,
cors,
logger,
json,
normalizeHeaders,
rateLimit,
safeHandler,
} from 'railiz'
import { cacheControl } from 'railiz-resilience'
import { useHttp, setGlobalHttpConfig } from 'railiz-http'
// Global config
setGlobalHttpConfig({
baseURL: 'https://jsonplaceholder.typicode.com',
cache: { ttl: 5000 },
timeout: 2000,
retry: 2,
})
// App setup
const app = new Railiz({ router: 'linear' })
app.use(normalizeHeaders())
app.use(bodyParser({ json: { limit: '2mb' } }))
app.use(logger())
app.use(cors({ origin: '*' }))
app.use(json())
app.use(rateLimit({ limit: 100, windowMs: 60000 }))
// Cache helper
function setCache(ctx: any, options: any) {
cacheControl({
set: (key, value) => ctx.set(key, value),
})(options)
}
// Home route (cache + swr)
app.get(
'/',
safeHandler(async (ctx) => {
const http = useHttp({ context: ctx })
const user = await http.get('/users/1', {
cache: {
ttl: 5000,
swr: 15000,
tags: ['users'],
},
})
const posts = await http.get('/posts', {
params: { _limit: 5 },
cache: {
ttl: 3000,
swr: 10000,
tags: ['posts'],
},
})
setCache(ctx, {
public: true,
maxAge: 5000,
swr: 10000,
})
return { user, posts }
}),
)
// User route
app.get(
'/users/:id',
safeHandler(async (ctx) => {
const http = useHttp({ context: ctx })
const user = await http.get(`/users/${ctx.params.id}`, {
cache: {
ttl: 10000,
key: `user:${ctx.params.id}`,
tags: ['users'],
},
})
setCache(ctx, {
public: true,
maxAge: 10000,
})
return { user }
}),
)
// Posts route
app.get(
'/posts',
safeHandler(async (ctx) => {
const http = useHttp({ context: ctx })
const posts = await http.get('/posts?_limit=10', {
cache: {
ttl: 5000,
swr: 20000,
tags: ['posts'],
},
})
setCache(ctx, {
public: true,
maxAge: 5000,
swr: 20000,
})
return { posts }
}),
)
// Checkout (breaker + retry)
app.get(
'/checkout',
safeHandler(async (ctx) => {
const http = useHttp({
context: ctx,
config: {
baseURL: 'https://api.payment.com',
},
})
const payment = await http.post(
'/charge',
{
amount: 100,
currency: 'USD',
},
{
breaker: {
key: 'payment-service',
failureThreshold: 3,
resetTimeout: 15000,
},
retry: {
attempts: 2,
delay: 200,
},
cache: false,
},
)
return { payment }
}),
)Standalone Usage (SDK mode)
import { createHttpContext, useHttp } from 'railiz-http'
const ctx = createHttpContext()
const http = useHttp({
context: ctx,
options: {
baseURL: 'https://jsonplaceholder.typicode.com',
},
})
async function main() {
const user = await http.get('/users/1', {
cache: { ttl: 5000 },
})
const posts = await http.get('/posts?_limit=3', {
cache: true,
})
console.log(user, posts)
}
main()Architecture
Railiz Context
↓
useHttp (merge config)
↓
HttpClient
↓
Middleware pipeline
↓
Memo (L1)
↓
Cache (L2)
↓
Retry / Timeout / Circuit Breaker
↓
fetch()When to use
- Microservices communication
- Backend aggregation layer (BFF)
- Payment / auth / external APIs
- Distributed system orchestration
When NOT to use
- Frontend data fetching (use React Query / SWR instead)
- Simple one-off scripts
- Static data requests without retry/caching needs
What makes it different?
Unlike traditional HTTP clients (Axios, fetch wrappers), railiz-http:
- Runs inside request context
- Supports multi-layer caching (L1 + L2)
- Designed for backend orchestration (not UI calls)
- Integrates with DI + middleware pipeline
- Enables deterministic request behavior
Philosophy
HTTP should be predictable, cached, and resilient by default.
License
MIT
