HTTP Router
Lightweight TypeScript decorators and a type-safe router for itty-router. Includes a build-time OpenAPI 3.1 generator. Ported from itty-decorators.
Features
Type-Safe Routing:
TypedRouterprovides full TypeScript support for request bodies, response types, and context.Auto JSON Enforcement: Automatically validates
Content-Type: application/jsonfor mutation methods (POST, PUT, PATCH).Multipart Support: Opt into
multipart/form-datahandling withMultipart<T>and{ multipart: true }.Declarative Metadata: Use
@Controllerand@Endpointdecorators to document your API logic directly in code.DI Integration:
@Controllercomposes the core DI@Containerdecorator, so controllers are auto-registered and can use@Componentinjection anduseContainer().resolve(...).OpenAPI 3.1 Support: Generate a complete OpenAPI specification from your code at build time.
Static assets:
HttpRouter.builder().static()serves a directory in development and packaged bytes after build.Minimal Footprint: Built on top of the ultra-light
itty-router.
Installation
Quick Start
1. Create a Controller (DI-aware)
Annotate your API logic using decorators and the TypedRouter. Controllers are automatically registered with the DI container, so you can inject services and resolve the controller instance.
2. Multipart File Uploads
Use Multipart<T> and { multipart: true } to accept multipart/form-data instead of JSON. The handler receives req.content typed as FormData.
3. Path and Query Parameters
Use PathParams<T> and QueryParams<T> combined with RequestSpec<...> to provide strong typing for URL path parameters (e.g., /user/:id) and query string parameters (e.g., ?search=term). The handler will receive them in req.params and req.query.
OpenAPI Generation
@di-framework/http provides typed APIs and a registry for generating OpenAPI specs from your controllers. Terminal routing and presentation belong to the unified di-framework CLI.
Using the CLI
Generate a spec through the canonical CLI command:
Options:
--controllers <path>: (Required) Path to the file that imports all your decorated controllers.--output <path>: (Optional) Path to save the generated JSON (default:openapi.json).
Manual Generation
You can also generate the spec programmatically using the generateOpenAPI function and the default registry:
If you need full control, you can iterate the registry manually:
API Reference
TypedRouter<Args[]>()
A proxy for itty-router that enables type-safe method definitions.
Args: An array of types representing additional arguments passed tofetch(e.g.,[Env, ExecutionContext]).
json<T>(data: T, init?: ResponseInit)
A typed wrapper around itty-router's json helper.
Json<T> / Multipart<T>
Body spec markers used with RequestSpec<> to declare the expected content type. Json<T> types req.content as T; Multipart<T> types it as FormData. Multipart routes require passing { multipart: true } as the third argument to the route method.
PathParams<T> / QueryParams<T>
Spec markers used with RequestSpec<> to declare the expected type of path and query parameters. PathParams<T> types req.params as T; QueryParams<T> types req.query as T.
@Controller(options?)
Composed decorator that:
Marks a class for inclusion in the OpenAPI registry; and
Registers the class with the core DI container (same instance as
@di-framework/core).
Options: { singleton?: boolean; container?: DIContainer }
@Endpoint(metadata)
Method or property decorator that attaches OpenAPI metadata.
summary: Short summary of the operation.description: Verbose explanation.requestBody: OpenAPI Request Body object.responses: OpenAPI Responses object.
HttpRouter.builder() & @HttpRouter(options?)
An extensible, fluent builder above TypedRouter() and its corresponding decorator:
prefix(pathPrefix): Sets a global base path prefix for registered routes.catch(handler): Registers a custom global error handler.use(...middleware): Registers global middleware executed on all routes.static(prefix, options): Serve files from a directory or a packaged bundle. See Static assets.withAuth(options): Extension point for auth integrations without introducing runtime dependencies in@di-framework/http.extend(fn): Register custom builder extensions.
Static assets
Declare an asset directory once. During development the handler reads the directory on each request. After packaging, the same mount serves in-memory bytes when the source directory is gone.
TypedRouter() has no .static() method. Mounts live on HttpRouter.builder()/ the built router, or @HttpRouter({ static }).
Local serving
The http-router example mounts public/ at /static next to POST /echo and GET /:
GET /static/style.css and HEAD /static/info.json serve from disk. Edits appear without rebuilding while live mode is on.
Options
Option | Behavior |
|---|---|
| Required. Live root, and registry lookup key if no |
|
|
| Copied to |
| Object or JSON path (native only). Metadata-only manifests still need a live directory for bytes. |
| In-memory bundle with encoded contents. Used when not in live mode. |
| Force disk vs package. Default: live iff the directory exists and there are no packaged contents. Packaged contents win unless |
Hidden path segments (names starting with .) 404. Directory listings are disabled. Symlinks that escape the real root are skipped at package time and 403/404 at serve time. There is no configurable exclusions option.
Builder prefix('/api') plus .static('/assets', …) mounts at /api/assets.
GET, HEAD, types, ETag
Only GET and HEAD. Other methods return 405 + Allow: GET, HEAD (or fallthrough).
Case | Status |
|---|---|
File found | 200 |
Matching | 304, empty body |
Missing / hidden / directory / no source | 404 |
| 403 (malformed |
Wrong method | 405 |
MIME comes from a built-in map; unknown types are application/octet-stream. Text types include charset=utf-8.
ETag: packaged files use a strong tag from SHA-256; live disk uses a weak W/"<size>-<mtime>-<ctime>" tag (no per-request hash). If-None-Match supports exact tags, W/-stripped compare, comma lists, and *.
Live GET streams from the file descriptor and omits Content-Length (the file can change while streaming). Live HEAD and packaged GET/HEAD set Content-Length. Consumers must read or cancel() the body so the descriptor closes.
Route order, middleware, and 404
HttpRouterBuilder.build() registers, in order: .use() middleware as all('*'), then .static() mounts, then withAuth/.extend(), then application get/post after .build().
Global middleware runs before the static handler.
withAuthRoutesdoes not wrap already-mounted static routes. Guard assets with.use(guard)orfallthrough: trueplus a later guarded route.Default
fallthrough: falsemeans a missing static path 404s and later routes never run.The example uses
fallthrough: truesoGET /andPOST /echocoexist. Static usesall, so non-GET/HEAD also need fallthrough to reachrouter.post(...).
Packaging
Native @di-framework/http only (not the portable entry):
packageStaticAssets({ directory, outputDir?, outFile?, format?, prefix? }) writes manifest.json/static-assets.json or a JS/TS module that calls registerStaticAssets. Manifests include posix keys, contentType, size, SHA-256 hash, and a strong etag.
Missing source directory at package time throws Error: Directory not found: …. Mounting .static() does not validate the directory; a missing live root is 404 (or fallthrough) at request time.
The wasmCloud plugin does not auto-discover .static() directories. Package on the build host, then either pass package: pkg into .static() or import generated JS that registers the bundle. The portable / wasmcloud entry has no node:fs and no packageStaticAssets. Serve without the source directory by using packaged contents (tests delete the directory and still GET/HEAD/304).
Out of scope (not implemented): SPA fallback, index-file routing, compression, byte ranges, remote storage, CDN provisioning, frontend compilation, configurable exclude globs, and automatic CLI collection of asset directories.
Next steps
wasmCloud - Component build; package assets on the host before bundling
CLI -
http openapi generateDeployment - Target runtimes