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

@xof/app

v0.0.8

Published

A lightweight and minimal HTTP application framework for Node.js, built on the native node:http module.

Readme

@xof/app

npm version downloads

A lightweight and minimal HTTP application framework for Node.js, built directly on top of the native node:http module.

@xof/app provides a simple and chainable API for building HTTP servers with routing, middleware, request body parsing, response helpers, request context, and structured error handling — without unnecessary abstractions.

Features

  • Lightweight HTTP framework
  • Built on Node.js native node:http
  • HTTP method routing
  • Dynamic route parameters
  • Global and route-level middleware
  • Async middleware support
  • Error-handling middleware
  • Built-in request body parsing
  • Configurable request body limits
  • Query string parsing
  • Response helpers
  • JSON responses
  • Redirect support
  • Per-request context
  • Structured application errors
  • Chainable API
  • Server lifecycle management
  • Optional static file serving
  • Zero unnecessary dependencies

Installation

Install @xof/app using npm:

npm install @xof/app

Quick Start

Create a basic HTTP application:

const xof = require('@xof/app');

const app = xof();

app.get('/', (req, res) => {
    res.send('Hello!');
});

app.start();

By default, the server listens on port 3000.

You can also specify a port:

const xof = require('@xof/app');

const app = xof();

app.get('/', (req, res) => {
    res.send('Hello, World!');
});

app.listen(8080);

Application

Create an application instance by calling xof():

const xof = require('@xof/app');

const app = xof();

The application accepts an optional configuration object:

const app = xof({
    port: 8080,
    host: '127.0.0.1',
    bodyLimit: '2mb',
    notallow: true
});

Configuration

| Option | Type | Default | Description | |---|---|---|---| | port | number | 3000 | Port used by the HTTP server | | host | string | undefined | Host/interface to bind | | bodyLimit | number \| string | 1mb | Maximum request body size | | notallow | boolean | false | Enable 405 Method Not Allowed responses |

Port

The port must be an integer between 0 and 65535.

const app = xof({
    port: 8080
});

Port 0 can be used to let the operating system select an available port.

Host

Specify the interface or hostname the server should bind to:

const app = xof({
    host: '127.0.0.1',
    port: 3000
});

Body Limit

The default request body limit is 1mb.

Human-readable units are supported:

const app = xof({
    bodyLimit: '5mb'
});

Supported units:

  • b
  • kb
  • mb
  • gb

Numbers are interpreted as bytes:

const app = xof({
    bodyLimit: 1024 * 1024
});

Static Files

Serve static files from a directory using the same HTTP server. Static files are served only when no backend route matches the request. Static serving is opt-in: @xof/app does not assume that ./public, ./dist, or any other directory exists, and it does not mount a static directory unless you call app.use(name, directory, options). If the configured directory is absent, static lookup simply falls through as an unmatched request.

const xof = require('@xof/app');

const app = xof({
    port: 8080
});

app.get('/client', (req, res) => {
    res.json({
        source: 'backend'
    });
});

app.use('public', './dist', {
    prefix: '/',
    maxAge: 3600,
    exclude: ['/api', '/client']
});

app.start();

Options

| Option | Type | Default | Description | |---|---|---|---| | prefix | string | '\/public' | URL mount point for static files | | index | string \| false | 'index.html' | Directory index filename, or false to disable directory indexes | | maxAge | number | 0 | Cache lifetime in seconds | | immutable | boolean | false | Append immutable to the cache policy when maxAge is greater than zero | | exclude | string[] | ['/api'] | URL paths excluded from static serving |

URL Prefix

The prefix option determines the URL path used to access static files.

app.use('public', './dist', {
    prefix: '/'
});

With prefix: '/', files inside ./dist are served from the site root.

For example:

  • ./dist/index.html → /
  • ./dist/style.css → /style.css
  • ./dist/assets/app.js → /assets/app.js

Excluded Paths

Use exclude to prevent specific URL paths from being served by the static handler.

app.use('public', './dist', {
    prefix: '/',
    exclude: ['/api', '/client']
});

When exclude is omitted, /api is excluded by default.

Excluded paths are intended for backend endpoints and other routes that should not fall through to static file serving.

Cache Control

The default maxAge is 0, which uses:

Cache-Control: no-cache

Set a cache lifetime in seconds:

app.use('public', './dist', {
    maxAge: 3600
});

Enable immutable caching for versioned assets:

app.use('public', './dist', {
    maxAge: 31536000,
    immutable: true
});

Request Handling

  • Supports GET and HEAD requests.
  • Matching backend routes take priority over static files.
  • A backend route that responds with 404 or 500 is not replaced by a static response.
  • Unmatched requests can fall back to static files.
  • Excluded paths are not served by the static handler.
  • Static file paths are constrained to the configured directory.

Routing

@xof/app provides routing for common HTTP methods.

GET

app.get('/users', (req, res) => {
    res.json({
        status: true,
        data: []
    });
});

POST

app.post('/users', (req, res) => {
    res.json({
        status: true,
        data: req.body
    });
});

PUT

app.put('/users/:id', (req, res) => {
    res.json({
        status: true,
        data: {
            id: req.params.id
        }
    });
});

PATCH

app.patch('/users/:id', (req, res) => {
    res.send('User updated');
});

DELETE

app.delete('/users/:id', (req, res) => {
    res.send('User deleted');
});

HEAD

app.head('/users', (req, res) => {
    res.end();
});

OPTIONS

app.options('/users', (req, res) => {
    res.json({
        status: true
    });
});

All Methods

Use .all() to register the same handlers for all supported HTTP methods:

app.all('/health', (req, res) => {
    res.json({
        status: true,
        message: 'OK'
    });
});

Route Parameters

Dynamic route parameters can be defined using the :name syntax.

app.get('/users/:id', (req, res) => {
    res.json({
        id: req.params.id
    });
});

A request to:

/users/123

makes the parameter available through req.params:

req.params.id;
// "123"

Multiple parameters are supported:

app.get('/users/:userId/posts/:postId', (req, res) => {
    res.json({
        userId: req.params.userId,
        postId: req.params.postId
    });
});

Route parameters are automatically URL-decoded.


Query Parameters

Query parameters are available through req.query.

For example:

/users?page=2&limit=10

can be accessed with:

app.get('/users', (req, res) => {
    res.json({
        status: true,
        data: {
            page: req.query.page,
            limit: req.query.limit
        }
    });
});

Repeated query parameters are represented as arrays.

For example:

/users?id=1&id=2&id=3

results in:

req.query.id;
// ["1", "2", "3"]

Query parameters without a value are represented as an empty string.


Middleware

Global middleware can be registered using .use().

app.use(async (req, res, next) => {
    console.log(`${req.method} ${req.path}`);

    await next();
});

Middleware can be synchronous or asynchronous.

Multiple middleware handlers can be registered at once:

app.use(
    middlewareOne,
    middlewareTwo,
    middlewareThree
);

.use() is chainable:

app.use(middlewareOne).use(middlewareTwo).get('/', handler);

Route Middleware

Middleware can also be attached directly to a route:

app.get('/profile', authenticate, authorize,(req, res) => {
        res.send('Profile');
    }
);

Global middleware runs before route middleware.


Error Middleware

Error-handling middleware uses four arguments:

app.use(async (error, req, res, next) => {
    console.error(error);

    await next(error);
});

A complete example:

const xof = require('@xof/app');

const app = xof();

app.get('/error', (req, res) => {
    throw new Error('Something went wrong');
});

app.use((error, req, res, next) => {
    console.error(error);

    res.status(500).json({
        status: false,
        data: {
            message: 'Something went wrong'
        }
    });
});

app.start();

If an error reaches the framework without being handled, @xof/app normalizes the error and generates a structured JSON response.


Request Body

@xof/app automatically reads request bodies for supported non-multipart requests.

Supported content types include:

  • application/json
  • application/*+json
  • text/plain
  • application/x-www-form-urlencoded
  • Other content types as raw Buffer

JSON

For a JSON request:

POST /users
Content-Type: application/json

{
    "name": "Kyxzz"
}

The parsed data is available through req.body:

app.post('/users', (req, res) => {
    res.json({
        status: true,
        data: req.body
    });
});

Text

For:

Content-Type: text/plain

the request body is returned as a string:

app.post('/message', (req, res) => {
    console.log(req.body);

    res.send('Received');
});

URL-Encoded

For:

Content-Type: application/x-www-form-urlencoded

the body is returned as a URLSearchParams instance:

app.post('/login', (req, res) => {
    const username = req.body.get('username');
    const password = req.body.get('password');

    res.json({
        status: true,
        username
    });
});

Raw Body

Unsupported content types are returned as a Buffer:

app.post('/upload', (req, res) => {
    console.log(Buffer.isBuffer(req.body));

    res.send('Received');
});

Empty Body

If the request has no body:

req.body === undefined;

Form Data and File Uploads

req.form() synchronously returns form fields as a plain object. You can also use property-style access with req.form.prompt or destructure directly from req.form. It supports application/x-www-form-urlencoded directly. For multipart/form-data, use @xof/multer as route-level middleware first; the form accessor then exposes text fields parsed by Multer. Multipart uploads are deliberately not buffered by the framework, so uploaded files can be processed as a stream.

const xof = require('@xof/app');
const multer = require('@xof/multer');

const app = xof();
const upload = multer({
    storage: multer.memoryStorage(),
    limits: {
        fileSize: 4 * 1024 * 1024,
        fields: 20
    }
});

app.post('/upload', upload.single('file'), (req, res) => {
    const form = req.form();
    const { prompt, width = '1024', height = '1024' } = form;
    // Property-style access is also available: const prompt = req.form.prompt;
    const file = req.file; // undefined when the optional file field is omitted

    res.json({
        status: true,
        data: { prompt, width, height, hasFile: Boolean(file) }
    });
});

app.start();

Use upload.single('file') when the file is optional or required on a field named file; omit the file from a request and Multer will still parse its text fields. Use upload.none() for a multipart form that must not contain files. Keep the upload middleware on the route that accepts the upload instead of registering upload.any() globally.

Example request without a file:

curl -i -X POST http://127.0.0.1:3000/upload \
  -F 'prompt=anime style' \
  -F 'width=1024' \
  -F 'height=1024'

req.form() and req.form.fieldName return form values synchronously after parsing. Values are strings; convert values such as width and height to numbers if the downstream function requires numeric input. Repeated application/x-www-form-urlencoded fields are returned as arrays. For multipart forms, repeated-field behavior follows @xof/multer.

Accessing req.form() or req.form.fieldName for multipart data before a multipart parser has run throws ERR_MULTIPART_PARSER_REQUIRED. The framework does not implement a second multipart parser because that would duplicate file parsing and buffering behavior.


Static Files

Serve a frontend directory from the same HTTP server. Static files are checked only when no backend route matches the request.

const app = require('@xof/app')({ port: 8080 });

app.get('/client', (req, res) => {
  res.json({ source: 'backend' });
});

app.use('public', './dist', {
  prefix: '/',
  maxAge: 3600
});

app.start();

Options:

  • prefix: URL mount point. Defaults to /public. Use / to serve files from the site root.
  • index: directory index filename. Defaults to index.html; set to false to disable directory indexes.
  • maxAge: cache lifetime in seconds. Defaults to 0 (Cache-Control: no-cache).
  • immutable: append immutable to the cache policy when maxAge is greater than zero.

GET and HEAD requests are supported. A matching backend route takes priority over static files. A backend route that responds with 404 or 500 is not replaced by a static response. Static file paths are constrained to the configured directory.

Response

The res object provides a convenient wrapper around Node.js ServerResponse.

res.status()

Set the HTTP response status:

app.get('/created', (req, res) => {
    res.status(201).send('Created');
});

The method is chainable:

res
    .status(201)
    .type('text/plain')
    .send('Created');

res.statusCode

Get or set the status code:

res.statusCode = 201;

console.log(res.statusCode);

res.set()

Set a response header:

res.set('X-Powered-By', '@xof/app');

res.header()

Alias for res.set():

res.header('X-Custom-Header', 'value');

res.get()

Read a response header:

const value = res.get('Content-Type');

res.remove()

Remove a response header:

res.remove('X-Powered-By');

res.type()

Set the Content-Type header:

res.type('application/json');

res.json()

Send a JSON response:

res.json({
    status: true,
    data: {
        message: 'Success'
    }
});

The response uses:

Content-Type: application/json; charset=utf-8

and automatically sets Content-Length.

res.send()

Send response data:

res.send('Hello, World!');

Strings automatically receive:

Content-Type: text/plain; charset=utf-8

Objects are automatically serialized as JSON:

res.send({
    status: true,
    message: 'Success'
});

res.write()

Write response data directly:

res.write('Hello ');
res.write('World');
res.end();

res.end()

End the response:

res.end('Done');

res.flushHeaders()

Flush response headers:

res.set('X-Server', 'XOF');
res.flushHeaders();

res.redirect()

Redirect the client:

res.redirect('/login');

The default status code is 302.

A custom redirect status can be specified:

res.redirect(301, '/new-location');

Context

Every request receives its own Context instance.

The context is available as the fourth argument of normal middleware and route handlers:

app.use((req, res, next, ctx) => {
    console.log(ctx);

    next();
});

ctx.req

The current request:

console.log(ctx.req);

ctx.res

The current response:

console.log(ctx.res);

ctx.route

The matched route:

console.log(ctx.route);

ctx.params

The current route parameters:

console.log(ctx.params);

ctx.set()

Store request-scoped state:

app.use((req, res, next, ctx) => {
    ctx.set('user', {
        id: 123
    });

    next();
});

ctx.get()

Retrieve stored state:

app.use((req, res, next, ctx) => {
    const user = ctx.get('user');

    console.log(user);

    next();
});

ctx.has()

Check whether a state value exists:

if (ctx.has('user')) {
    console.log('User exists');
}

ctx.delete()

Delete a state value:

ctx.delete('user');

ctx.error

Contains the normalized error while processing an error middleware pipeline.

ctx.elapsed()

Returns the elapsed request processing time as a BigInt value in nanoseconds:

app.use((req, res, next, ctx) => {
    console.log(`${ctx.elapsed()}ns`);

    next();
});

Error Handling

@xof/app includes a structured application error system.

Built-in HTTP Errors

The framework handles common HTTP errors including:

  • 400 Bad Request
  • 404 Not Found
  • 405 Method Not Allowed
  • 413 Payload Too Large
  • 500 Internal Server Error

404 Not Found

If no route matches the request, the framework returns a structured error response:

{
    "status": false,
    "data": {
        "message": "Cannot GET /unknown"
    }
}

405 Method Not Allowed

When a path exists but the requested HTTP method does not match, @xof/app can return 405 Method Not Allowed.

This behavior is controlled by the notallow option.


notallow

By default, an unsupported method on an existing route is treated as 404.

const app = xof();

Enable 405 Method Not Allowed responses with:

const app = xof({
    notallow: true
});

For example, if the application contains:

app.get('/users', (req, res) => {
    res.send('Users');
});

and receives:

POST /users

the framework can respond with:

HTTP/1.1 405 Method Not Allowed
Allow: GET

The Allow header contains the methods registered for the matched path.


Body Limit Errors

When a request body exceeds the configured bodyLimit, the framework rejects the request with HTTP 413.

Example:

const app = xof({
    bodyLimit: '1mb'
});

A request exceeding the limit results in an error with:

Status: 413
Code: ERR_BODY_TOO_LARGE

Server Lifecycle

app.start()

Start the HTTP server using the configured port and host:

app.start();

A callback can be provided:

app.start((server) => {
    console.log('Server started');
});

The callback receives the native Node.js http.Server instance.

app.listen()

Start the server with an optional port override:

app.listen(8080);

Or:

app.listen(8080, (server) => {
    console.log('Server started');
});

If no port is provided, the configured application port is used.

app.close()

Close the server:

app.close(() => {
    console.log('Server closed');
});

Native Node.js Server

The underlying http.Server instance is available through app.server.

const server = app.server;

console.log(server);

This allows integration with native Node.js server APIs when required.


Application Configuration

The current application configuration can be accessed through app.config.

const app = xof({
    port: 8080,
    host: '127.0.0.1',
    bodyLimit: '2mb'
});

console.log(app.config);

Complete Example

The following example demonstrates routing, middleware, route parameters, query parameters, JSON requests, responses, and error handling:

const xof = require('@xof/app');

const app = xof({
    port: 3000,
    bodyLimit: '1mb'
});

app.use(async (req, res, next, ctx) => {
    const started = Date.now();
    await next();
    console.log(`${req.method} ${req.path} - ${Date.now() - started}ms`);
});

app.get('/', (req, res) => {
    res.json({
        status: true,
        message: 'App is running'
    });
});

app.get('/users', (req, res) => {
    res.json({
        status: true,
        data: {
            page: req.query.page || '1'
        }
    });
});

app.get('/users/:id', (req, res) => {
    res.json({
        status: true,
        data: {
            id: req.params.id
        }
    });
});

app.post('/users', (req, res) => {
    res.status(201).json({
        status: true,
        data: req.body
    });
});

app.use((error, req, res, next) => {
    console.error(error);

    if (res.writableEnded) {
        return;
    }

    res.status(500).json({
        status: false,
        data: {
            message: 'Internal Server Error'
        }
    });
});

app.start((server) => {
    console.log('server running on port 3000');
});

API Reference

Application

| API | Description | |---|---| | xof(options) | Create an application | | app.use() | Register middleware | | app.all() | Register a route for all supported methods | | app.get() | Register a GET route | | app.post() | Register a POST route | | app.put() | Register a PUT route | | app.patch() | Register a PATCH route | | app.delete() | Register a DELETE route | | app.head() | Register a HEAD route | | app.options() | Register an OPTIONS route | | app.start() | Start the HTTP server | | app.listen() | Start the server with an optional port | | app.close() | Close the HTTP server | | app.config | Access application configuration | | app.server | Access the native http.Server |

Response

| API | Description | |---|---| | res.status() | Set HTTP status | | res.statusCode | Get/set HTTP status | | res.set() | Set response header | | res.header() | Alias for set() | | res.get() | Get response header | | res.remove() | Remove response header | | res.type() | Set response content type | | res.json() | Send JSON | | res.send() | Send response data | | res.write() | Write response data | | res.end() | End response | | res.flushHeaders() | Flush response headers | | res.redirect() | Redirect the client |

Context

| API | Description | |---|---| | ctx.req | Current request | | ctx.res | Current response | | ctx.route | Matched route | | ctx.params | Route parameters | | ctx.state | Request-scoped state | | ctx.error | Current normalized error | | ctx.set() | Store state | | ctx.get() | Retrieve state | | ctx.has() | Check state existence | | ctx.delete() | Delete state | | ctx.elapsed() | Get elapsed processing time |


Design Philosophy

@xof/app is designed around a few principles.

Native First

Built directly on Node.js's native HTTP server, keeping the framework lightweight and close to the platform.

Minimal API

The public API focuses on essential HTTP application features without introducing unnecessary abstractions.

Chainable

Application configuration and routing can be composed cleanly:

app.use(logger).use(auth).get('/users', usersHandler);

Async-Friendly

Middleware and request processing work naturally with async / await.

Predictable Errors

Errors are normalized into a consistent response structure while internal errors remain protected from accidental exposure.


Changelog

Changelog

License

MIT © XOF