@xof/app
v0.0.8
Published
A lightweight and minimal HTTP application framework for Node.js, built on the native node:http module.
Keywords
Readme
@xof/app
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/appQuick 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:
bkbmbgb
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-cacheSet 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
GETandHEADrequests. - Matching backend routes take priority over static files.
- A backend route that responds with
404or500is 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/123makes 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=10can 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=3results 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/jsonapplication/*+jsontext/plainapplication/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/plainthe 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-urlencodedthe 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 toindex.html; set tofalseto disable directory indexes.maxAge: cache lifetime in seconds. Defaults to0(Cache-Control: no-cache).immutable: appendimmutableto the cache policy whenmaxAgeis 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-8and automatically sets Content-Length.
res.send()
Send response data:
res.send('Hello, World!');Strings automatically receive:
Content-Type: text/plain; charset=utf-8Objects 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 Request404 Not Found405 Method Not Allowed413 Payload Too Large500 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 /usersthe framework can respond with:
HTTP/1.1 405 Method Not Allowed
Allow: GETThe 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_LARGEServer 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.
