feather-server
v0.1.0
Published
Feather Server: tiny Express-like Host() helper for quick HTTP servers
Maintainers
Readme
Feather Server – tiny HTTP server helper
Host() in Feather Server is a minimal Express-style helper built on Node's http module. It offers simple routing with path params, JSON bodies, and response helpers.
Install
npm install feather-serverQuick start
Create a small server:
const { Host, securityHeaders, rateLimit, cors, validateBody, compress } = require('feather-server');
const app = Host();
app.get('/hello', (_req, res) => {
res.json({ message: 'Hello there!' });
});
app.get('/users/:id', (req, res) => {
res.json({ userId: req.params.id, query: req.query });
});
app.post('/echo', (req, res) => {
res.json({ youSent: req.body });
});
// Add common security headers (CSP/HSTS/nosniff/etc.)
app.use(securityHeaders());
// Rate limit requests (60/min default) and lock CORS to an allowlist
app.use(rateLimit({ windowMs: 60_000, max: 100, headers: true }));
app.use(cors({ origins: ['http://localhost:3000'], methods: ['GET', 'POST'] }));
app.use(compress({ threshold: 1024 })); // gzip/br for larger responses
// Structured logs + request IDs: use Host({ structuredLogs: true, redactErrors: true })
// Serve everything under ./public at /public (cached 1 hour by default)
app.static('/public', './public');
app
.listen(3000)
.then(() => {
console.log('Server running on http://localhost:3000');
})
.catch((err) => {
console.error('Failed to start', err);
});API at a glance
Host(options)→ app with routing/middleware; options include limits, logging, metrics, CORS, multipart, etc.- Routing:
app.get/post/put/delete(path, handler, { bodyLimit? }),app.route(base).get(...).post(...),app.prefix('/api', subApp); supports path params, wildcards, regex. - Middleware:
app.use(fn); Error handlers:app.onError(fn). - Built-ins:
securityHeaders(),rateLimit(),cors(),compress(),cookies(),validateBody(),static()for files. - Responses:
res.status,res.statusMessage,res.json,res.send,res.text,res.type,res.set,res.redirect,res.render. - Request helpers:
req.params,req.query,req.body,req.form,req.files,req.rawBody,req.rawBuffer,req.cookies,req.signedCookies,req.requestId,req.log. - Lifecycle:
app.listen(...)(Promise),app.close()(Promise),app.metrics(); built-in/health, optional/metrics.
Features:
- Methods:
get,post,put,delete - Path params:
/users/:id - JSON body parsing when
Content-Type: application/json - Middleware:
app.use((req, res, next) => { ...; next(); }) - Error hook:
app.onError((err, req, res, next) => { ... }) - Static files:
app.static('/public', './public', { maxAge: 3600 })(GET/HEAD, index.html support) - Route chaining:
app.route('/users').get(...).post(...) - Prefix mounting:
app.prefix('/api', subApp); supports path params, wildcards, and regex routes - Helpers:
res.status(code),res.statusMessage(text),res.json(data),res.send(data),res.text(text),res.set(name, value),res.type(mime),res.redirect(url, code=302),res.render(view, data) - Async safety: rejected promises auto-500 with stack logging
- Form parsing:
req.formforapplication/x-www-form-urlencoded - Configurable body limit via
Host({ bodyLimit: <bytes> })(default ~1MB) and per-route override; restrict content types withallowedContentTypes - Multipart:
req.body.fields/req.filesfor multipart/form-data (in-memory or temp files) - Lifecycle:
app.listen(...).then(server => ...),app.close().then(...) - Built-in logger: timing, status colors, enabled when
NODE_ENV !== 'test'(configurable) - Tests:
npm test(node:test) - Typings: ships
index.d.tsfor TS/IDE support - CLI scaffold:
npx feather-server new my-app [--port=3000] - Security headers helper:
app.use(securityHeaders()) - Timeouts/limits:
Host({ headerTimeout, requestTimeout, keepAliveTimeout, maxConnections, maxHeaderSize }) - Rate limiting helper:
app.use(rateLimit({ windowMs, max }))(in-memory; supports custom store, rate-limit headers) - Strict CORS helper:
app.use(cors({ origins: [...] })) - Body validation helper:
validateBody(schema)(simple JSON-schema-like) - Request IDs + structured logs:
Host({ requestIdHeader, structuredLogs, redactErrors, redactHeaders }) - Metrics & health:
app.metrics(), built-in/health, optionalmetricsPath - Compression helper:
app.use(compress({ threshold })) - Cookie helper:
app.use(cookies())
Static files
// Mounts ./public at /public with caching
app.static('/public', './public', { maxAge: 86400 });Notes:
- Looks for exact files or
index.htmlwhen requesting a directory. - Prevents path traversal outside the mounted directory.
- Sets
Cache-Control: public, max-age=<seconds>andLast-Modified. - Supports GET and HEAD requests.
Middleware
// Basic CORS
app.use((_req, res, next) => {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Headers', 'Content-Type');
next();
});Middlewares run before static serving and routes. Call next() to continue; skip next() to short-circuit.
Hardened starter config
const { Host, securityHeaders, rateLimit, cors, compress, cookies } = require('./');
const app = Host({
env: 'production',
logger: true,
structuredLogs: true,
redactErrors: true,
redactHeaders: ['authorization', 'cookie', 'set-cookie'],
allowedContentTypes: ['application/json'],
multipart: { enabled: false }, // enable only if needed
headerTimeout: 10000,
requestTimeout: 20000,
keepAliveTimeout: 5000,
maxConnections: 1000,
metricsPath: '/metrics',
prometheusMetrics: true,
});
app.use(securityHeaders());
app.use(rateLimit({ windowMs: 60_000, max: 100, headers: true }));
app.use(cors({ origins: ['https://your-site.com'], strict: true }));
app.use(compress({ threshold: 1024 }));
app.use(cookies({ secrets: ['current', 'previous'], signed: true }));
app.get('/health', (_req, res) => res.json({ ok: true }));Logging
- Default dev logger prints
METHOD path -> status timewith colors when TTY. - Enabled unless
NODE_ENV === 'test'. Override withHost({ logger: false })orHost({ logger: true, colors: false }). - Structured logs:
Host({ structuredLogs: true, redactErrors: true, redactHeaders })outputs JSON with request IDs; redacts headers by default (Authorization/Cookie). - Custom handler:
Host({ logHandler: (level, entry) => ... })to send logs to your sink. - AsyncLocal context: access the current store via
app.context()(inside handlers/middleware) to retrieverequestId,req, orres.
Health & metrics
- Built-in
GET /healthreturns{ status: 'ok' }(disable withHost({ enableHealth: false }), path viahealthPath). app.metrics()returns counts and average duration.
CLI scaffold
npx feather-server new my-app --port=4000
cd my-app
npm install
npm startGenerates server.js using Host() and a package.json with a start script.
Security headers
// Default set: HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, CORP, CSP
app.use(securityHeaders());
// Custom CSP or HSTS off
app.use(securityHeaders({
contentSecurityPolicy: "default-src 'self'; img-src 'self' data:",
hsts: false,
}));Rate limiting
// 100 requests per minute per IP (token bucket)
app.use(rateLimit({ windowMs: 60_000, max: 100, headers: true }));
// Custom store (e.g., Redis) and no headers
// app.use(rateLimit({ windowMs: 60_000, max: 100, headers: false, store: { get: async (k)=>..., set: async (k,v)=>... } }));- Key defaults to
X-Forwarded-Foror socket address. - Override limit action with
onLimit(req, res, next). - In-memory store is single-process; use a custom store for clustered deployments.
CORS
app.use(
cors({
origins: ['https://example.com', 'http://localhost:3000'],
methods: ['GET', 'POST'],
headers: ['Content-Type'],
credentials: true,
maxAge: 600,
strict: true, // deny by default
denyStatus: 403,
denyMessage: 'Origin not allowed',
}),
);- Strict allowlist for origins; OPTIONS preflight handled with allowed methods/headers.
Metrics
- Enable a metrics endpoint with
Host({ metricsPath: '/metrics' })(returns JSON with totals/status buckets/avg duration). - Customize response via
metricsHandler(req, res, data)or consumeapp.metrics()programmatically. - Prometheus format:
Host({ metricsPath: '/metrics', prometheusMetrics: true })to emit plain text exposition.
Compression
// Apply gzip/br when Accept-Encoding allows and payload >= threshold
app.use(compress({ threshold: 2048 }));Multipart limits
- Configure in
Host({ multipart: { enabled, maxFileSize, maxFiles, tempDir } }). - If
tempDiris set (path or'os'), uploaded files are written there andreq.files[*].pathis set;datamay be omitted to save memory. Temp files are cleaned after request completion. - Files exceeding
maxFileSizeor counts exceedingmaxFilesreturn 413. - Set
enabled: falseto reject multipart with 415.
Rendering
const app = Host({
render: (view, data) => `<html><body><h1>${view}</h1><pre>${JSON.stringify(data)}</pre></body></html>`,
});
app.get('/page', (req, res) => {
res.render('Hello', { name: req.query.name || 'world' });
});Validation
app.post(
'/signup',
validateBody({
type: 'object',
required: ['email', 'password'],
properties: {
email: { type: 'string' },
password: { type: 'string' },
newsletter: { type: 'boolean' },
},
}),
(req, res) => res.json({ ok: true, user: req.body }),
);- Lightweight validator for shapes (type/object/array), required fields, and nested properties.
- Invalid bodies return 400 with the validation error message.
Cookies
const { cookies } = require('./');
app.use(cookies({ secrets: ['old-key', 'new-key'], signed: true }));
app.get('/me', (req, res) => {
res.json({ cookies: req.cookies, signed: req.signedCookies });
});Use req.signCookie(value) to produce signed cookie values when setting cookies yourself.
Route grouping & prefixing
// Chain handlers on the same base path
app.route('/users')
.get((_req, res) => res.json([{ id: 1 }]))
.post(
validateBody({
type: 'object',
required: ['name'],
properties: { name: { type: 'string' } },
}),
(req, res) => res.json({ created: req.body }),
);
// Mount a sub-app under /api
const api = Host();
api.get('/status', (_req, res) => res.json({ ok: true }));
app.prefix('/api', api);route(base)chains methods on a single path.prefix('/api', subApp)runs anotherHost()instance with the path trimmed to the prefix.
Error handling
// Global error handler (runs on thrown errors or rejected promises)
app.onError((err, req, res, next) => {
console.error('Error on', req.path, err);
if (res.writableEnded) return;
res.status(err.status || 500).json({ error: err.message || 'Server error' });
// Call next(err) to delegate to another error handler.
});- Defaults to JSON
{ error: <message> }with status code fromerr.status/err.statusCodeor500. In production (exposeErrors: false) messages for 5xx are replaced withServer errorbut full stack is still logged. - Invalid JSON returns 400; oversized body returns 413.
- 404 responses return JSON
{ error: 'Not Found', path }. - Rejected promises in handlers/middleware are caught, logged, and return 500 by default.
- Form parsing errors return 400. Body size limit is configurable via
Host({ bodyLimit }). - Unsupported content types return 415 (allowed types configurable via
allowedContentTypes).
Body parsing
const app = Host({ bodyLimit: 2_000_000, multipart: { maxFileSize: 2_000_000, tempDir: 'os' } }); // 2MB limit
app.post('/submit', (req, res) => {
// JSON requests populate req.body (object) and req.rawBody (string)
// URL-encoded forms populate req.form (object) and req.body (same object)
res.json({ body: req.body, form: req.form });
});
app.get('/go-somewhere', (_req, res) => {
res.redirect('https://example.com');
});
// Per-route body limit override
app.post('/upload', (_req, res) => res.json({ ok: true }), { bodyLimit: 5_000_000 });
// Restrict content types (defaults: json, x-www-form-urlencoded, text/plain)
const strictApp = Host({ allowedContentTypes: ['application/json'] });
// Text/plain with charset
app.post('/text', (req, res) => {
res.json({ text: req.body, raw: req.rawBody });
});
// Multipart form-data (fields + files). Configure tempDir/maxFileSize in Host options.
app.post('/upload-form', (req, res) => {
res.json({
fields: req.body.fields,
files: req.files?.map((f) => ({ name: f.name, filename: f.filename, size: f.size, path: f.path })),
});
});
// Disable multipart entirely
const noMultipart = Host({ multipart: { enabled: false } });Lifecycle
const app = Host();
app.listen(3000).then((server) => {
console.log('Listening on 3000');
// Later, when you want to stop:
setTimeout(() => {
app.close().then(() => console.log('Closed'));
}, 10_000);
});listen returns a promise that resolves with the http.Server. close returns a promise and resolves even if the server is not running.
Notes
- Built only on core Node; no external dependencies.
- Bodies are limited to ~1MB by default to avoid runaway memory use.
