npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

feather-server

v0.1.0

Published

Feather Server: tiny Express-like Host() helper for quick HTTP servers

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-server

Quick 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.form for application/x-www-form-urlencoded
  • Configurable body limit via Host({ bodyLimit: <bytes> }) (default ~1MB) and per-route override; restrict content types with allowedContentTypes
  • Multipart: req.body.fields / req.files for 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.ts for 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, optional metricsPath
  • 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.html when requesting a directory.
  • Prevents path traversal outside the mounted directory.
  • Sets Cache-Control: public, max-age=<seconds> and Last-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 time with colors when TTY.
  • Enabled unless NODE_ENV === 'test'. Override with Host({ logger: false }) or Host({ 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 retrieve requestId, req, or res.

Health & metrics

  • Built-in GET /health returns { status: 'ok' } (disable with Host({ enableHealth: false }), path via healthPath).
  • app.metrics() returns counts and average duration.

CLI scaffold

npx feather-server new my-app --port=4000
cd my-app
npm install
npm start

Generates 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-For or 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 consume app.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 tempDir is set (path or 'os'), uploaded files are written there and req.files[*].path is set; data may be omitted to save memory. Temp files are cleaned after request completion.
  • Files exceeding maxFileSize or counts exceeding maxFiles return 413.
  • Set enabled: false to 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 another Host() 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 from err.status/err.statusCode or 500. In production (exposeErrors: false) messages for 5xx are replaced with Server error but 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.