npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

Readme

🧵✨ @rexeus/typeweaver-server

npm version License TypeScript

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, and InferState
  • Automatic HEAD handling — falls back to GET handlers per HTTP spec
  • 405 Method Not Allowed — with proper Allow header

📥 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 server

More 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 or instanceof discrimination 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 middlewarenext() 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