@rexeus/typeweaver-server
v0.7.0
Published
Generates a lightweight, dependency-free server with built-in routing and middleware from your API definitions. Powered by Typeweaver.
Maintainers
Readme
🧵✨ @rexeus/typeweaver-server
Typeweaver is a type-safe HTTP API framework built for API-first development with a focus on developer experience. Use typeweaver to specify your HTTP APIs in TypeScript and Zod, and generate clients, validators, routers, and more ✨
📝 Server Plugin
This plugin generates a lightweight, dependency-free server with built-in routing and middleware
from your typeweaver API definitions. No external framework required — everything runs on the
standard Fetch API (Request/Response).
For each resource, it produces a <ResourceName>Router class that registers routes, validates
requests, and wires your handler methods with full type safety. Mount routers on the provided
TypeweaverApp to get a complete server with middleware support.
Choose this plugin for a zero-dependency, Fetch API-native server. For Hono framework integration, see @rexeus/typeweaver-hono.
Key Features
- Zero runtime dependencies — no Hono, Express, or Fastify required
- Fetch API compatible — works with Bun, Deno, Cloudflare Workers, and Node.js (>=18)
- High-performance radix tree router — O(d) lookup where d = number of path segments
- Type-safe middleware — compile-time state guarantees via
defineMiddleware,StateMap, andInferState - Automatic HEAD handling — falls back to GET handlers per HTTP spec
- 405 Method Not Allowed — with proper
Allowheader
📥 Installation
# Install the CLI and the plugin as a dev dependency
npm install -D @rexeus/typeweaver @rexeus/typeweaver-server
# Install the runtime as a dependency
npm install @rexeus/typeweaver-core💡 How to use
npx typeweaver generate --input ./api/definition --output ./api/generated --plugins serverMore on the CLI in @rexeus/typeweaver.
📂 Generated Output
For a resource User, the plugin generates:
generated/
lib/server/ ← TypeweaverApp, middleware types, etc.
user/
UserRouter.ts ← Router class + ServerUserApiHandler type
GetUserRequest.ts ← Request types (IGetUserRequest)
GetUserResponse.ts ← Response types + factory classes
...Import TypeweaverApp, routers, and types from ./generated.
🚀 Usage
Implement handlers
Each handler receives the typed request and returns a typed response — plain objects with
statusCode, header, and body. Content-Type is auto-set to application/json for object
bodies.
// user-handlers.ts
import { HttpStatusCode } from "@rexeus/typeweaver-core";
import type { ServerUserApiHandler } from "./generated";
export const userHandlers: ServerUserApiHandler = {
async handleListUsersRequest() {
return {
statusCode: HttpStatusCode.OK,
body: [{ id: "1", name: "Jane", email: "[email protected]" }],
};
},
async handleCreateUserRequest(request) {
return {
statusCode: HttpStatusCode.CREATED,
body: { id: "1", name: request.body.name, email: request.body.email },
};
},
async handleGetUserRequest(request) {
return {
statusCode: HttpStatusCode.OK,
body: {
id: request.param.userId,
name: "Jane",
email: "[email protected]",
},
};
},
async handleDeleteUserRequest() {
return { statusCode: HttpStatusCode.NO_CONTENT };
},
};Generated response classes (e.g.
GetUserSuccessResponse) are also available for when you need runtime type checks orinstanceofdiscrimination in error handling.
Create the app
// server.ts
import { TypeweaverApp, UserRouter } from "./generated";
import { userHandlers } from "./user-handlers";
const app = new TypeweaverApp();
app.route(new UserRouter({ requestHandlers: userHandlers }));
export default app;Start the server
Bun
import app from "./server";
Bun.serve({ fetch: app.fetch, port: 3000 });Deno
import app from "./server.ts";
Deno.serve({ port: 3000 }, app.fetch);Node.js
import { createServer } from "node:http";
import { nodeAdapter } from "./generated/lib/server";
import app from "./server";
createServer(nodeAdapter(app)).listen(3000);Multiple routers
app.route(new UserRouter({ requestHandlers: userHandlers }));
app.route("/api/v1", new OrderRouter({ requestHandlers: orderHandlers }));🔗 Middleware
Middleware is defined with defineMiddleware and follows a return-based onion model. Each
middleware declares what state it provides downstream and what state it requires from
upstream — all checked at compile time.
Providing state — pass state to next():
import { defineMiddleware } from "./generated/lib/server";
const auth = defineMiddleware<{ userId: string }>(async (ctx, next) => {
const token = ctx.request.header?.["authorization"];
return next({ userId: parseToken(token) });
});When TProvides has keys, next() requires the state object as its argument — you can't forget
to provide it.
Requiring upstream state — declare dependencies:
const permissions = defineMiddleware<{ permissions: string[] }, { userId: string }>(
async (ctx, next) => {
const userId = ctx.state.get("userId"); // string — no cast, no undefined
return next({ permissions: await loadPermissions(userId) });
}
);Registering permissions before auth produces a compile-time error because userId is not
yet available in the accumulated state.
Pass-through middleware — next() takes no arguments:
const logger = defineMiddleware(async (ctx, next) => {
const start = Date.now();
const response = await next();
console.log(
`${ctx.request.method} ${ctx.request.path} -> ${response.statusCode} (${Date.now() - start}ms)`
);
return response;
});Short-circuit — return a response without calling next():
const guard = defineMiddleware(async (ctx, next) => {
if (!ctx.request.header?.["authorization"]) {
return { statusCode: 401, body: { message: "Unauthorized" } };
}
return next();
});Path-scoped guard — use pathMatcher to limit middleware to specific routes:
import { defineMiddleware, pathMatcher } from "./generated/lib/server";
const isUsersPath = pathMatcher("/users/*");
const usersGuard = defineMiddleware(async (ctx, next) => {
if (!isUsersPath(ctx.request.path)) return next();
if (!ctx.request.header?.["authorization"]) {
return { statusCode: 401, body: { message: "Unauthorized" } };
}
return next();
});pathMatcher supports exact matches ("/health") and prefix matches ("/users/*").
Chaining — state accumulates through .use():
const app = new TypeweaverApp()
.use(auth) // provides { userId: string }
.use(permissions) // requires { userId }, provides { permissions: string[] }
.route(new TodoRouter({ requestHandlers: todoHandlers }));InferState — extract the accumulated state type for handlers:
import type { InferState } from "./generated/lib/server";
type AppState = InferState<typeof app>;
// { userId: string } & { permissions: string[] }Middleware runs for all requests, including 404s and 405s, so global concerns like logging and CORS always execute.
📦 Built-in Middleware
Ready-to-use middleware included with the server plugin.
| Middleware | Description | State |
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | --------------- |
| cors | CORS headers & preflight handling | — |
| basicAuth | HTTP Basic Authentication | { username } |
| bearerAuth | HTTP Bearer Token Authentication | { token } |
| logger | Request/response logging with timing | — |
| secureHeaders | OWASP security headers | — |
| requestId | Request ID generation & propagation | { requestId } |
| poweredBy | X-Powered-By header | — |
| scoped / except | Path-based middleware filtering | — |
import { cors, logger, secureHeaders, bearerAuth, requestId } from "@rexeus/typeweaver-server";
const app = new TypeweaverApp()
.use(cors())
.use(secureHeaders())
.use(logger())
.use(requestId())
.use(bearerAuth({ verifyToken: verify }))
.route(new UserRouter({ requestHandlers }));Each middleware is documented in detail — click the links above.
🛠️ App Options
TypeweaverApp accepts an optional options object:
| Option | Type | Default | Description |
| ------------- | -------------------------- | ------------------ | ----------------------------------------------------------- |
| maxBodySize | number | 1_048_576 (1 MB) | Max request body size in bytes. Exceeding returns 413. |
| onError | (error: unknown) => void | console.error | Error callback. Falls back to console.error if it throws. |
const app = new TypeweaverApp({
maxBodySize: 5 * 1024 * 1024, // 5 MB
onError: error => logger.error("Unhandled error", error),
});⚙️ Router Configuration
Each router accepts TypeweaverRouterOptions:
| Option | Type | Default | Description |
| -------------------------- | ---------------------------- | ---------- | ---------------------------------- |
| requestHandlers | Server<Resource>ApiHandler | required | Handler methods for each operation |
| validateRequests | boolean | true | Enable/disable request validation |
| handleValidationErrors | boolean \| function | true | Handle validation errors |
| handleHttpResponseErrors | boolean \| function | true | Handle thrown HttpResponse |
| handleUnknownErrors | boolean \| function | true | Handle unexpected errors |
When set to true, error handlers use sensible defaults (400/500 responses). When set to false,
errors fall through to the next handler in the chain. When set to a function, it receives the error
and ServerContext and must return an IHttpResponse.
🚨 Error Handling
Throwing errors in handlers
All generated error response classes (e.g. NotFoundErrorResponse, ValidationErrorResponse)
extend HttpResponse. Throw them in your handlers — the framework catches them automatically:
import { HttpStatusCode } from "@rexeus/typeweaver-core";
import { GetUserSuccessResponse, NotFoundErrorResponse } from "./generated";
async handleGetUserRequest(request) {
const user = await db.findUser(request.param.userId);
if (!user) {
throw new NotFoundErrorResponse({
statusCode: HttpStatusCode.NOT_FOUND,
header: { "Content-Type": "application/json" },
body: { message: "Resource not found", code: "NOT_FOUND_ERROR" },
});
}
return new GetUserSuccessResponse({
statusCode: HttpStatusCode.OK,
header: { "Content-Type": "application/json" },
body: user,
});
}When handleHttpResponseErrors is true (the default), thrown HttpResponse instances are
returned as-is. No extra configuration needed.
Custom error mapping
Use custom handler functions to transform errors into your own response shape.
Validation errors — map framework validation errors to your spec-defined format:
new UserRouter({
requestHandlers: userHandlers,
handleValidationErrors: (error, ctx) =>
new ValidationErrorResponse({
statusCode: HttpStatusCode.BAD_REQUEST,
header: { "Content-Type": "application/json" },
body: {
code: "VALIDATION_ERROR",
message: "Request is invalid",
issues: {
body: error.bodyIssues,
query: error.queryIssues,
param: error.pathParamIssues,
header: error.headerIssues,
},
},
}),
});HTTP response errors — log thrown errors and pass them through:
new UserRouter({
requestHandlers: userHandlers,
handleHttpResponseErrors: (error, ctx) => {
logger.warn("HTTP error", {
status: error.statusCode,
path: ctx.request.path,
});
return error;
},
});Unknown errors — catch unexpected failures and return a safe response:
new UserRouter({
requestHandlers: userHandlers,
handleUnknownErrors: (error, ctx) => {
logger.error("Unhandled error", { error, path: ctx.request.path });
return new InternalServerErrorResponse({
statusCode: HttpStatusCode.INTERNAL_SERVER_ERROR,
header: { "Content-Type": "application/json" },
body: { code: "INTERNAL_SERVER_ERROR", message: "Something went wrong" },
});
},
});📋 Error Responses
| Status | Code | When |
| ------ | ----------------------- | ------------------------------------------------------------- |
| 400 | BAD_REQUEST | Malformed request body |
| 400 | Validation issues | handleValidationErrors: true and request fails validation |
| 404 | NOT_FOUND | No matching route |
| 405 | METHOD_NOT_ALLOWED | Route exists but method not allowed (includes Allow header) |
| 413 | PAYLOAD_TOO_LARGE | Request body exceeds maxBodySize |
| 500 | INTERNAL_SERVER_ERROR | Unhandled error in handler |
All error responses follow the shape: { code: string, message: string }.
📄 License
Apache 2.0 © Dennis Wentzien 2026
