@cuboapp/http-server
v1.0.12
Published
Minimal, type-safe HTTP server with routing, params, body parsing and auth for Node.js
Maintainers
Readme
@cuboapp/http-server
A minimal, type-safe HTTP server for Node.js built on the native node:http module. It adds pattern-based routing with named (and optional) parameters, automatic body parsing, query parsing, CORS, and a pluggable authorization hook — with full TypeScript inference for request/response shapes.
- Zero runtime dependencies — only Node's built-ins.
- ESM, ships type declarations.
- Type-safe routes — describe
Body/Params/Queryper route and get inference in the handler.
Install
npm install @cuboapp/http-serverRequires Node.js >= 18.
Quick start
import { createHttpServer } from '@cuboapp/http-server'
const server = createHttpServer({ host: '0.0.0.0', port: 3000, debug: true })
await server.registerRoute('GET', '/health', async () => ({ ok: true }))
await server.registerRoute('GET', '/users/:id', async ({ params }) => {
return { id: params.id }
})
await server.registerRoute('POST', '/users', async ({ body }) => {
return { code: 201, created: body }
})
await server.start()
// Http server started: http://0.0.0.0:3000Routing
Register routes with registerRoute(method, path, handler, opts?). Supported methods: GET, POST, PUT, PATCH, DELETE, OPTIONS.
Parameters
Use :name for a required parameter and :name? for an optional one:
await server.registerRoute('GET', '/posts/:id', async ({ params }) => params.id)
await server.registerRoute('GET', '/posts/:id?', async ({ params }) => params.id ?? 'all')A missing required parameter responds with 400.
Query
Query string values are parsed into ctx.query:
// GET /search?q=hello
await server.registerRoute('GET', '/search', async ({ query }) => ({ q: query.q }))Typed routes
Pass a DTO describing the route to get full inference for body, params, and query:
type CreateUser = {
Body: { name: string; email: string }
Params: { id: string }
Query: { invite?: string }
}
await server.registerRoute<CreateUser>('POST', '/users/:id', async ({ body, params, query }) => {
// body.name, params.id, query.invite are all typed
return { code: 201, id: params.id, name: body.name }
})Request body
For POST, PUT, and PATCH the body is read and parsed automatically into ctx.body:
application/x-www-form-urlencoded→ parsed object- anything else → parsed as JSON, falling back to the raw string if parsing fails
Reading the body times out after 2 seconds and responds with 408. To read the stream yourself (e.g. for uploads or streaming), set manualBody and use ctx.request directly:
await server.registerRoute('POST', '/upload', async ({ request }) => {
// consume `request` as a raw stream yourself
return { ok: true }
}, { manualBody: true })Responses
The value a handler returns determines the HTTP response:
| Return value | Status | Body |
| --- | --- | --- |
| string / number / boolean | 200 | the value as text |
| null / undefined | 200 | empty |
| array | 200 | JSON |
| object | code ?? 200 | JSON of the remaining fields |
For an object response, the reserved keys code, message, and headers are interpreted and removed from the JSON body; everything else is serialized:
async () => ({
code: 201, // -> HTTP status
headers: { contentType: 'application/json; charset=utf-8' },
id: 1, // -> body: { "id": 1, "name": "Ada" }
name: 'Ada'
})Writing the response manually
To take full control of the response (custom streaming, redirects, etc.), write to ctx.response and return { raw: true }. The server will not touch the response afterwards:
await server.registerRoute('GET', '/stream', async ({ response }) => {
response.writeHead(200, { 'Content-Type': 'text/plain' })
response.end('streamed')
return { raw: true }
})Errors
Throw an object with code and message to send an error response:
await server.registerRoute('GET', '/secret', async () => {
throw { code: 403, message: 'Forbidden' }
})Unmatched routes respond with 404. CORS preflight (OPTIONS) requests are answered automatically with 204 and permissive CORS headers.
Authorization
Provide an auth handler and opt routes in (or default all routes in). The handler receives the server and request, and can attach data to request.auth or throw to reject:
type Ctx = { auth: { userId: string } }
const server = createHttpServer<Ctx>({
host: '0.0.0.0',
port: 3000,
auth: {
default: false, // set true to require auth on every route unless overridden
handler: async ({ request }) => {
const token = request.headers['authorization']
if (!token) throw { code: 401, message: 'Unauthorized' }
request.auth = { userId: 'resolved-from-token' }
}
}
})
// opt a single route into auth
await server.registerRoute('GET', '/me', async ({ auth }) => ({ userId: auth.userId }), { authorize: true })API
createHttpServer<C>(options)
Creates a server instance.
| Option | Type | Description |
| --- | --- | --- |
| host | string | Host to bind. |
| port | number | Port to listen on. |
| debug | boolean | Log startup and request errors. |
| auth.default | boolean | Require auth on all routes by default. |
| auth.handler | (ctx) => void \| Promise<void> | Authorization hook. |
HttpServer methods
registerRoute(method, path, handler, opts?)— register a route.optsaccepts{ authorize?, manualBody? }.start()— initialize and begin listening.stop()— close the server.init()— build the underlyinghttp.Serverwithout listening.getInstance()— the underlyingnode:httpServer(available afterinit()/start()).
httpRoute(method, path, handler, opts?)
A small helper to declare a route descriptor separately from a server instance, useful for collecting routes across modules:
import { httpRoute } from '@cuboapp/http-server'
export const getHealth = httpRoute('GET', '/health', async () => ({ ok: true }))
// later, against a server:
await server.registerRoute(getHealth.method, getHealth.path, getHealth.handler, getHealth.opts)License
MIT © CuboSoft
