A high-performance file-based web framework with seamless Fastify compatibility and native real-time capabilities
Build APIs and real-time endpoints with a clean folder structure and fast iteration.
Oweb is a route-per-file framework built on Fastify, with an optional uWebSockets runtime mode for high-throughput workloads.
You keep Fastify compatibility and plugin ergonomics, while getting a cleaner architecture, built-in HMR, and first-class SSE/WebSocket support.
- Dual runtime: Fastify-compatible default runtime + optional
uWebSockets.jsruntime - File-based routing
- Dynamic params (
[id]), method files (.post,.put, ...), and matcher params ([id=integer]) - Hierarchical hooks via
_hooks.js/_hooks.ts - Route-level and global error handling
- Built-in HMR for routes and matchers
- SSE via iterable / async-iterable route handlers
- WebSocket routes with
WebSocketRoute - Fastify plugin ecosystem support
- TypeScript-first codebase and typings
npm install owebjsUse the same Oweb API in both modes.
import Oweb from 'owebjs';
const app = await new Oweb().setup();import Oweb from 'owebjs';
const app = await new Oweb({ uWebSocketsEnabled: true }).setup();When to prefer uWebSocketsEnabled: true:
- very high connection count
- aggressive WebSocket usage
- throughput-focused deployments
Machine: Windows 11, Ryzen 5 5500, 16GB RAM, 6C/12T, SSD (Base 3.60 GHz, boost ~4.1 GHz observed)
Method: autocannon -c 100 -d 40 -p 10 localhost:3000 * 2, taking the
second average
| Runtime | Version | Requests/sec |
|---|---|---|
| uWebSockets.js | 20.52.0 | 79,149 |
| Oweb (uWS) | 1.5.8-dev | 76,853 |
| 0http | 4.4.0 | 46,605 |
| Fastify | 4.23.2 | 46,238 |
| Oweb (Fastify) | 1.5.8-dev | 42,570 |
| Node.js http.createServer | 24.5.0 | 42,544 |
| H3 (v2 API) | 2.0.1-rc.14 | 36,063 |
| Express | 5.2.1 | 24,913 |
This is a synthetic "Hello, Word!" benchmark that aims to evaluate the framework overhead. The overhead that each framework has on your application depends on your application. You should always benchmark if performance matters to you.
For the highest throughput, use these defaults:
- Enable uWebSockets runtime:
uWebSocketsEnabled: true - Disable powered-by header:
poweredByHeader: false - For headers that should be sent on every request (for example CORS-related headers), use
staticResponseHeadersinstead of per-request hooks
Example (production-oriented):
import Oweb from 'owebjs';
const app = await new Oweb({
uWebSocketsEnabled: true,
poweredByHeader: false,
autoPreflight: true,
staticResponseHeaders: {
// CORS (set your real origin in production)
'access-control-allow-origin': 'https://yourdomain.com',
'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
'access-control-allow-headers': 'Content-Type, Authorization',
vary: 'Origin',
// Security headers
'x-content-type-options': 'nosniff',
'x-frame-options': 'DENY',
'referrer-policy': 'strict-origin-when-cross-origin',
'permissions-policy': 'geolocation=(), microphone=(), camera=()',
'cross-origin-opener-policy': 'same-origin',
'cross-origin-resource-policy': 'same-site',
},
}).setup();
await app.loadRoutes({
directory: 'routes',
hmr: {
enabled: false,
},
});
await app.start({ port: 3000, host: '0.0.0.0' });Use autoPreflight to return 204 for preflight requests and set CORS headers through staticResponseHeaders.
import Oweb from 'owebjs';
const app = await new Oweb({
uWebSocketsEnabled: true,
autoPreflight: true,
poweredByHeader: false,
staticResponseHeaders: {
'access-control-allow-origin': 'https://yourdomain.com',
'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
'access-control-allow-headers': 'Content-Type, Authorization',
vary: 'Origin',
},
}).setup();If you need credentials, do not use * for access-control-allow-origin; set an explicit origin.
Start with a minimal app, then we will add route conventions step by step.
import Oweb from 'owebjs';
const app = await new Oweb({ uWebSocketsEnabled: true }).setup();
await app.loadRoutes({
directory: 'routes',
hmr: {
enabled: true,
},
});
const { err, address } = await app.start({ port: 3000, host: '127.0.0.1' });
if (err) throw err;
console.log(`Server running at ${address}`);What this does:
- Creates an Oweb app instance (here in uWebSockets mode)
- Loads your route files from
routes/ - Enables HMR for development
- Starts the HTTP server
The file system is the routing table. Here is a representative structure:
routes/
_hooks.js
hello.js
users/
[id].js
auth/
login.post.js
posts/
[id=integer].js
events/
sse.js
ws/
echo.js
matchers/
integer.jsNow let's go through each convention in isolation.
Use a normal file for a GET route.
routes/hello.js -> GET /hello
import { Route } from 'owebjs';
export default class HelloRoute extends Route {
handle() {
return { message: 'hello-world' };
}
}Put parameter names in brackets.
routes/users/[id].js -> GET /users/:id
import { Route } from 'owebjs';
export default class UserRoute extends Route {
handle(req) {
return { id: req.params.id };
}
}Use filename suffixes when an endpoint is not GET.
routes/auth/login.post.js -> POST /auth/login
Supported suffixes: .get, .post, .put, .patch, .delete
import { Route } from 'owebjs';
export default class LoginPostRoute extends Route {
handle(req, res) {
return res.status(201).send({ method: req.method, body: req.body });
}
}Matcher params add filename-level validation.
routes/posts/[id=integer].js + matchers/integer.js
// matchers/integer.js
export default function integerMatcher(value) {
return /^-?\d+$/.test(String(value));
}Then register the matcher directory:
await app.loadRoutes({
directory: 'routes',
matchersDirectory: 'matchers',
hmr: {
enabled: true,
matchersDirectory: 'matchers',
},
});If the matcher returns false, the route is treated as not matched.
Hooks are defined with _hooks.js and run for that folder scope.
Example root hook:
import { Hook } from 'owebjs';
export default class RootHook extends Hook {
handle(req, _res, done) {
req.locals ??= {};
req.locals.trace = ['root'];
done();
}
}Key behavior:
- Hooks apply to the current directory and child directories
- Nested folders can add more hooks
- Hooks run before the route handler
- Scoped folders using parentheses (like
(admin)) creates a hook boundary
Oweb's route walker inspects parent hook paths and looks for the nearest folder whose name is wrapped in parentheses (for example (api) or (admin)).
When such a folder exists, hook resolution is cut at that scope boundary. In practice, hooks above that scoped folder are excluded.
Example:
routes/
_hooks.js # global hook
(admin)/
_hooks.js # admin scope boundary hook
users/
_hooks.js
[id].jsFor routes/(admin)/users/[id].js, Oweb uses hooks inside that scoped chain and stops climbing above the (admin) boundary.
You can handle errors globally, or per route when you need custom behavior.
app.setInternalErrorHandler((req, res, error) => {
res.status(500).send({
source: 'global-handler',
message: error.message,
});
});import { Route } from 'owebjs';
export default class RouteWithCustomError extends Route {
handle() {
throw new Error('route-specific-error');
}
handleError(_req, res, err) {
return res.status(409).send({
source: 'route-handleError',
message: err.message,
});
}
}Use route-level handling when you want endpoint-specific status codes or payload shape.
If a route returns an iterable or async iterable, Oweb streams it as SSE.
import { setTimeout as delay } from 'node:timers/promises';
import { Route } from 'owebjs';
export default class SseRoute extends Route {
async *handle(req, res) {
if (req.query?.deny === '1') {
return res.status(401).send({ code: 'error.unauthorized' });
}
yield 'event-1';
await delay(20);
yield { step: 2 };
await delay(20);
yield 3;
}
}This is useful for live feeds, progress updates, and long-running operations.
Create a file that exports a class extending WebSocketRoute.
routes/ws/echo.js -> WS /ws/echo
import { WebSocketRoute } from 'owebjs';
export default class EchoSocketRoute extends WebSocketRoute {
open(ws) {
ws.send('ready');
}
message(ws, message, isBinary) {
ws.send(message, isBinary);
}
}Works in both runtime modes:
- Fastify WebSocket adapter (default)
- Native
uWebSockets.jsmode
Enable HMR while loading routes:
await app.loadRoutes({
directory: 'routes',
matchersDirectory: 'matchers',
hmr: {
enabled: true,
directory: 'routes',
matchersDirectory: 'matchers',
},
});Notes:
- HMR is disabled in
NODE_ENV=production - Route hook files (
_hooks.js/_hooks.ts) are not hot-reloaded - For hook changes, restart the server
Because Oweb sits on Fastify, you can register Fastify plugins directly.
import multipart from '@fastify/multipart';
await app.register(multipart, {
limits: {
fileSize: 10 * 1024 * 1024,
},
});Oweb exposes ChunkUpload from owebjs/plugins for chunked upload workflows.
import { Route } from 'owebjs';
import { ChunkUpload, ChunkUploadStatus } from 'owebjs/plugins';
export default class ChunkUploadRoute extends Route {
async handle(req, res) {
const file = await req.file();
const buffer = await file.toBuffer();
const result = await ChunkUpload(
{
buffer,
fileName: file.filename,
currentChunk: Number(req.query.currentChunk),
totalChunks: Number(req.query.totalChunks),
},
{
path: './uploads',
maxChunkSize: 1024 * 1024,
maxFileSize: 10 * 1024 * 1024,
},
);
if (
result.status === ChunkUploadStatus.ChunkTooLarge ||
result.status === ChunkUploadStatus.FileTooLarge
) {
return res.status(413).send(result);
}
return res.send(result);
}
}Oweb supports .ts route files and exports framework typings. You can keep the same file conventions and class model in TypeScript projects.
MIT