erestrust
v0.2.0
Published
Express response helpers, rate limiting, CORS, security headers & request IDs — all built-in, zero external dependencies.
Downloads
252
Maintainers
Readme
The Problem
Every Express project reinvents the same response patterns:
// Without erestrust — inconsistent, repetitive
res.status(200).json({ success: true, data: user });
res.status(404).json({ error: "not found" });
res.status(422).json({ errors: [{ field: 'email', msg: 'invalid' }] });// With erestrust — clean, typed, consistent
res.ok(user);
res.notFound('User not found');
res.invalid([{ field: 'email', message: 'Invalid format' }]);Installation
npm install erestrustPeer dependency: Express ≥ 4.0
Quick Start
import express from 'express';
import { erestrust } from 'erestrust';
const app = express();
app.use(erestrust());
// ✅ 200 OK
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) return res.notFound('User not found');
res.ok(user);
});
// ✅ 201 Created
app.post('/users', async (req, res) => {
const user = await db.createUser(req.body);
res.created(user);
});
// ✅ 204 No Content
app.delete('/users/:id', async (req, res) => {
await db.deleteUser(req.params.id);
res.noContent();
});
// ✅ Paginated
app.get('/users', async (req, res) => {
const [users, total] = await db.findUsers(req.query);
res.paginated(users, { page: 1, perPage: 20, total });
});
// ✅ Validation errors
app.post('/login', async (req, res) => {
res.invalid([
{ field: 'email', message: 'Required' },
{ field: 'password', message: 'Min 8 characters' },
]);
});Response Methods
Success (2xx)
| Method | Status | Description |
|--------|--------|-------------|
| res.ok(data, meta?) | 200 | Success response |
| res.created(data, meta?) | 201 | Resource created |
| res.accepted(data, meta?) | 202 | Request accepted |
| res.noContent() | 204 | No content |
| res.paginated(data, pagination, meta?) | 200 | Paginated list |
Client Errors (4xx)
| Method | Status | Description |
|--------|--------|-------------|
| res.badRequest(message?, code?) | 400 | Bad Request |
| res.unauthorized(message?, code?) | 401 | Unauthorized |
| res.forbidden(message?, code?) | 403 | Forbidden |
| res.notFound(message?, code?) | 404 | Not Found |
| res.methodNotAllowed(message?, code?) | 405 | Method Not Allowed |
| res.conflict(message?, code?) | 409 | Conflict |
| res.gone(message?, code?) | 410 | Gone |
| res.invalid(errors, message?) | 422 | Validation Error |
| res.tooManyRequests(message?, retryAfter?) | 429 | Too Many Requests |
Server Errors (5xx)
| Method | Status | Description |
|--------|--------|-------------|
| res.serverError(message?, code?) | 500 | Internal Server Error |
| res.badGateway(message?, code?) | 502 | Bad Gateway |
| res.serviceUnavailable(message?, code?) | 503 | Service Unavailable |
| res.gatewayTimeout(message?, code?) | 504 | Gateway Timeout |
Generic
| Method | Description |
|--------|-------------|
| res.fail(status, message, code?, details?) | Any error status |
Response Shapes
Success
{
"success": true,
"data": { "id": 1, "name": "Alice" }
}Error
{
"success": false,
"error": {
"status": 404,
"message": "User not found",
"code": "USER_NOT_FOUND"
}
}Paginated
{
"success": true,
"data": [...],
"pagination": {
"page": 1,
"perPage": 20,
"total": 100,
"totalPages": 5,
"hasNext": true,
"hasPrev": false
}
}Error Handler
Catch thrown errors globally:
import { errorHandler } from 'erestrust';
app.use(errorHandler());
// or with options:
app.use(errorHandler({
logger: (err, status) => myLogger.error(err),
maskServerErrors: true, // hides 5xx messages in production (default)
}));Security Middleware
erestrust ships with 4 zero-dependency security middleware that cover the most common Express hardening needs.
Rate Limiting
In-memory rate limiter using a token-bucket algorithm. No Redis required.
import { rateLimit } from 'erestrust';
// 100 requests per minute (default)
app.use(rateLimit());
// Custom: 10 requests per 15 seconds on auth routes
app.use('/api/auth', rateLimit({ max: 10, windowMs: 15_000 }));
// Skip health checks
app.use(rateLimit({ skip: (req) => req.path === '/health' }));Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| max | number | 100 | Max requests per window |
| windowMs | number | 60000 | Time window in ms |
| message | string | 'Too Many Requests' | Error message when exceeded |
| keyGenerator | (req) => string | req.ip | Key to identify clients |
| skip | (req) => boolean | () => false | Skip rate limiting for a request |
| headers | boolean | true | Include RateLimit-* headers |
CORS
Zero-dependency CORS middleware.
import { cors } from 'erestrust';
// Reflect request origin (default)
app.use(cors());
// Specific origins
app.use(cors({ origin: ['https://example.com', 'https://app.example.com'] }));
// Dynamic origin with credentials
app.use(cors({
origin: (origin) => origin?.endsWith('.example.com') ? origin : false,
credentials: true,
}));Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| origin | string \| string[] \| function | reflects request | Allowed origins |
| methods | string[] | ['GET','HEAD','PUT','PATCH','POST','DELETE'] | Allowed methods |
| allowedHeaders | string[] | inferred | Allowed request headers |
| exposedHeaders | string[] | none | Headers exposed to browser |
| credentials | boolean | false | Allow credentials |
| maxAge | number | 86400 | Preflight cache max age (seconds) |
| preflightContinue | boolean | true | Respond 204 to OPTIONS |
Security Headers
Sets common security headers on every response. Helmet-like, zero dependencies.
import { securityHeaders } from 'erestrust';
// Sensible defaults
app.use(securityHeaders());
// Customize
app.use(securityHeaders({
hsts: 'max-age=63072000; includeSubDomains; preload',
contentSecurityPolicy: "default-src 'self'; script-src 'self' cdn.example.com",
}));Default headers set:
| Header | Default Value |
|--------|---------------|
| X-Content-Type-Options | nosniff |
| X-Frame-Options | DENY |
| X-XSS-Protection | 0 (modern best practice) |
| Strict-Transport-Security | max-age=31536000; includeSubDomains |
| Referrer-Policy | strict-origin-when-cross-origin |
| Content-Security-Policy | default-src 'self' |
| X-Permitted-Cross-Domain-Policies | none |
| X-DNS-Prefetch-Control | off |
| X-Powered-By | removed |
Request ID
Attaches a unique request ID to every request/response. Reads from the incoming header (if trusted) or generates a new UUID.
import { requestId } from 'erestrust';
app.use(requestId());
app.get('/ping', (req, res) => {
console.log(req.id); // '550e8400-e29b-41d4-a716-446655440000'
res.ok({ pong: true });
});Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| header | string | 'X-Request-Id' | Header to read/write |
| generator | () => string | crypto.randomUUID() | ID generator function |
| trustProxy | boolean | true | Trust incoming request ID header |
Full Setup Example
import express from 'express';
import { erestrust, errorHandler, cors, rateLimit, securityHeaders, requestId } from 'erestrust';
const app = express();
// Security middleware
app.use(requestId());
app.use(securityHeaders());
app.use(cors({ origin: 'https://myapp.com', credentials: true }));
app.use(rateLimit({ max: 100, windowMs: 60_000 }));
// Response helpers
app.use(erestrust());
// Routes
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) return res.notFound('User not found');
res.ok(user);
});
// Error handler (last)
app.use(errorHandler());TypeScript
All methods are fully typed. To get full autocompletion on res.* helpers, add the type augmentation import:
// In your app entry or a global .d.ts file:
import 'erestrust/express';
// Now res.ok(), res.created(), res.notFound(), etc. are typed on Express `Response`The middleware itself:
import { erestrust, errorHandler, cors, rateLimit, securityHeaders, requestId } from 'erestrust';Exported Types
import type {
HttpStatusCode,
FieldError,
ApiSuccess,
ApiError,
ApiPaginated,
ApiResponse,
ErestrustMethods,
ErrorHandlerOptions,
RateLimitOptions,
CorsOptions,
SecurityHeadersOptions,
RequestIdOptions,
} from 'erestrust';| Type | Description |
|------|-------------|
| HttpStatusCode | Union of supported HTTP status codes |
| FieldError | { field: string; message: string; code?: string } |
| ApiSuccess<T> | { success: true; data: T; meta?: Record<string, unknown> } |
| ApiError | { success: false; error: { status, message, code?, details?, errors? } } |
| ApiPaginated<T> | Success shape with data: T[] and pagination object |
| ApiResponse<T> | Union of ApiSuccess<T> \| ApiError \| ApiPaginated<T> |
| ErestrustMethods | Interface describing all response helper methods |
| ErrorHandlerOptions | Options for errorHandler() — logger, maskServerErrors |
| RateLimitOptions | Options for rateLimit() — max, windowMs, keyGenerator, etc. |
| CorsOptions | Options for cors() — origin, methods, credentials, etc. |
| SecurityHeadersOptions | Options for securityHeaders() — per-header toggles |
| RequestIdOptions | Options for requestId() — header, generator, trustProxy |
STATUS_TEXT
A lookup object mapping HTTP status codes to their standard text:
import { STATUS_TEXT } from 'erestrust';
STATUS_TEXT[200]; // 'OK'
STATUS_TEXT[404]; // 'Not Found'
STATUS_TEXT[500]; // 'Internal Server Error'License
MIT
