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

@stitchem/http

v1.0.0

Published

HTTP layer for stitchem. Extends `@stitchem/core` with routers, middleware, guards, pipes, exception filters, validation, and response serialization — all wired through the DI container.

Readme

@stitchem/http

HTTP layer for stitchem. Extends @stitchem/core with routers, middleware, guards, pipes, exception filters, validation, and response serialization — all wired through the DI container.

Requirements

  • @stitchem/core >= 1.0.0
  • TypeScript >= 5.2 (TC39 decorators)
  • Node.js >= 22 or any runtime with AsyncLocalStorage

Install

pnpm add @stitchem/http

You also need an adapter. The Hono adapter is currently the only supported one:

pnpm add @stitchem/hono

Quick Start

import { module } from '@stitchem/core';
import { HttpServer, router, get } from '@stitchem/http';
import { hono } from '@stitchem/hono';

@router('/hello')
class HelloRouter {
  @get('/')
  index() {
    return { message: 'Hello, world!' };
  }
}

@module({ routers: [HelloRouter] })
class AppModule {}

const app = await HttpServer.create(AppModule, {
  adapter: hono({ server: 'bun' }),
});

await app.listen(3000);

Table of Contents


HttpServer

HttpServer.create() is the entry point. It initializes the DI context, discovers routers, and returns a MergedHttpServer — a Proxy that exposes both HttpServer methods and the full native adapter API.

const app = await HttpServer.create(AppModule, {
  adapter: hono({ server: 'bun' }),
  logging: { level: LogLevel.INFO }, // optional
});

// HttpServer methods
await app.listen(3000);
app.get(UserService);        // DI resolution shorthand
app.use(cors());             // native or stitchem middleware
app.useGlobal({ guards: [AuthGuard] });

// Native adapter methods pass through directly
app.use(cors());
app.get('/path', handler);   // native Hono route (2+ args)

HttpServerOptions

| Option | Description | |--------|-------------| | adapter | HTTP adapter instance (required) | | logging? | false | { logger } | { level } — same as Context.create |

HttpServer Methods

| Method | Description | |--------|-------------| | HttpServer.create(module, options) | Creates the server and initializes the DI context | | app.listen(port, hostname?) | Binds routes and starts listening | | app.init() | Binds routes without starting (useful for testing) | | app.close() | Stops the server and disposes the DI context | | app.get(token) | Resolves a provider from the DI container | | app.use(...handlers) | Registers global middleware (stitchem or native) | | app.useGlobal({ filters?, pipes?, guards? }) | Registers global filters, pipes, and guards |


Routing

Routers are plain classes decorated with @router(). They receive full DI via static inject or @inject().

import { injectable } from '@stitchem/core';
import { router, get, post, del, HttpContext } from '@stitchem/http';

@router('/users')
class UserRouter {
  static inject = [UserService] as const;
  constructor(private users: UserService) {}

  @get('/')
  async list(ctx: HttpContext) {
    return ctx.json(await this.users.findAll());
  }

  @get('/:id')
  async get(ctx: HttpContext) {
    const id = ctx.param('id');
    return ctx.json(await this.users.findById(id));
  }

  @post('/')
  async create(ctx: HttpContext) {
    const body = await ctx.body<{ name: string }>();
    const user = await this.users.create(body);
    return ctx.status(201).json(user);
  }

  @del('/:id')
  async delete(ctx: HttpContext) {
    await this.users.delete(ctx.param('id'));
    // returning nothing → 204 No Content
  }
}

Register routers in any module's routers array:

@module({ routers: [UserRouter], providers: [UserService] })
class UserModule {}

HTTP Method Decorators

| Decorator | HTTP Method | |-----------|-------------| | @get(path?) | GET | | @post(path?) | POST | | @put(path?) | PUT | | @patch(path?) | PATCH | | @del(path?) | DELETE | | @options(path?) | OPTIONS | | @head(path?) | HEAD |

All decorators default path to '/'.

HttpContext

The portable request/response object passed to every route handler and middleware:

// Request
ctx.request                    // Web standard Request
ctx.param('id')                // route parameter
ctx.param()                    // all route parameters
ctx.query('page')              // query parameter (qs-parsed)
ctx.query()                    // all query parameters
ctx.header('authorization')    // request header
await ctx.body<CreateUserDto>() // parsed request body

// Response state (chainable)
ctx.status(201)
ctx.header('X-Custom', 'value')

// Response builders
ctx.json(data)                 // JSON response
ctx.text('ok', 200)            // plain text response

// Context store
ctx.set('user', user)          // typed via HttpContextStore augmentation
ctx.get('user')                // retrieve
ctx.get('correlation')         // always available (built-in)

// Escape hatch
ctx.native<HonoContext>()      // access native framework context

HttpContextStore

Typed per-request key–value store. Augment the interface to get type safety across middleware and handlers:

declare module '@stitchem/http' {
  interface HttpContextStore {
    user: User;
    tenantId: string;
  }
}

// Now typed everywhere:
ctx.set('user', user);         // ✓
ctx.get('user');               // → User

correlation (UUID v7) is always available — automatically set by the built-in correlation middleware on every request.

Return Values

| Handler return | Response sent | |----------------|---------------| | Response | Returned directly — bypasses serialization | | Any value | ctx.json(value) — or serialized via @serialize if applied | | void / null / undefined | 204 No Content |


Request Pipeline

Every request flows through this pipeline (outer to inner):

Filters (catch errors from everything inside)
  └── Middleware (global → class → method)
        └── Pipes (validation → method → class → global)
              └── Guards (method → class → global)
                    └── Handler
  • Middleware runs first in request direction, for cross-cutting concerns
  • Pipes validate and transform request data before authorization
  • Guards perform authorization after data is validated
  • Filters wrap the entire pipeline and catch thrown exceptions

A UUID v7 correlation ID is automatically prepended as the first global middleware on every route — always accessible via ctx.get('correlation').


Middleware

Class Middleware

Implement the Middleware interface and decorate with @middleware():

import { middleware, Middleware, HttpContext } from '@stitchem/http';

@middleware()
class LoggerMiddleware implements Middleware {
  use(ctx: HttpContext, next: () => Promise<void>) {
    console.log(`${ctx.request.method} ${ctx.request.url}`);
    return next();
  }
}

Apply to a router (all routes) or a specific route:

@router('/users')
@useMiddlewares(LoggerMiddleware)       // all routes in this router
class UserRouter {
  @post('/')
  @useMiddlewares(RateLimitMiddleware)  // this route only
  create(ctx: HttpContext) { ... }
}

Class middleware supports full DI:

@middleware()
class AuthMiddleware implements Middleware {
  static inject = [AuthService] as const;
  constructor(private auth: AuthService) {}

  async use(ctx: HttpContext, next: () => Promise<void>) {
    const token = ctx.header('authorization');
    if (!token) throw new UnauthorizedException();
    ctx.set('user', await this.auth.verify(token));
    return next();
  }
}

Inline Middleware

Use defineMiddleware() for lightweight, DI-free middleware:

import { defineMiddleware } from '@stitchem/http';

const timingMiddleware = defineMiddleware(async (ctx, next) => {
  const start = Date.now();
  await next();
  ctx.header('X-Response-Time', `${Date.now() - start}ms`);
});

Apply inline middleware the same way as class middleware:

@useMiddlewares(timingMiddleware)
class UserRouter { ... }

HttpModule consumer pattern

For fine-grained route matching, implement the HttpModule interface on a module class:

import { HttpModule, MiddlewareConsumer } from '@stitchem/http';

@module({ routers: [UserRouter, AdminRouter] })
class AppModule implements HttpModule {
  middleware(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes(UserRouter, AdminRouter);

    consumer
      .apply(AuthMiddleware)
      .forRoutes(AdminRouter)
      .exclude('GET /admin/health');
  }
}

forRoutes() and exclude() accept router classes or 'METHOD /path' strings.

Global Middleware

Register middleware that runs on every route before app.listen():

app.use(cors());                          // native framework middleware
app.use(i18n.detect());                   // stitchem middleware

app.use() accepts both. Stitchem middleware (created via defineMiddleware) is bridged through the adapter; native middleware is passed directly to the framework.


Guards

Guards control access to routes. They run after pipes, before the handler. Returning false throws ForbiddenException; throwing an exception sends it to the filter chain.

Class Guards

import { guard, Guard, HttpContext } from '@stitchem/http';

@guard()
class AuthGuard implements Guard {
  static inject = [AuthService] as const;
  constructor(private auth: AuthService) {}

  async canActivate(ctx: HttpContext): Promise<boolean> {
    const token = ctx.header('authorization');
    if (!token) throw new UnauthorizedException();
    ctx.set('user', await this.auth.verify(token));
    return true;
  }
}

Apply with @useGuards():

@router('/admin')
@useGuards(AuthGuard)                // all routes in this router
class AdminRouter {
  @del('/users/:id')
  @useGuards(AdminRoleGuard)         // this route only
  deleteUser(ctx: HttpContext) { ... }
}

Inline Guards

Pass a GuardFunction directly to @useGuards() for simple cases:

@get('/me')
@useGuards((ctx: HttpContext) => ctx.get('user') !== undefined)
getProfile(ctx: HttpContext) { ... }

Global Guards

app.useGlobal({ guards: [AuthGuard] });

Guard resolution order per request: method → class → global. All guards must pass.


Pipes

Pipes run before guards to validate and transform request data. A pipe's transform() method can optionally return a ResponseTransform function that runs after the handler (applied in reverse pipe order).

Class Pipes

import { Pipe, HttpContext, ResponseTransform } from '@stitchem/http';

class TimingPipe implements Pipe {
  transform(ctx: HttpContext): ResponseTransform {
    const start = Date.now();
    return (response: Response) => {
      response.headers.set('X-Response-Time', `${Date.now() - start}ms`);
      return response;
    };
  }
}

Apply with @usePipes():

@router('/users')
@usePipes(new TimingPipe())             // all routes
class UserRouter {
  @post('/')
  @usePipes(new MyValidationPipe())    // this route only
  create(ctx: HttpContext) { ... }
}

Pipes are pre-constructed — they receive no DI. Pass instances or functions directly.


Validation

@validate

Use @validate() to attach Standard Schema validation to a route method. Validation runs first in the pipe chain. Errors from all sources are aggregated and thrown as a single ValidationException:

import { z } from 'zod';

@router('/users')
class UserRouter {
  @post('/')
  @validate({
    body: z.object({ email: z.string().email(), name: z.string().min(1) }),
    query: z.object({ notify: z.string().optional() }),
  })
  create(ctx: HttpContext) { ... }

  @get('/')
  @validate({ query: z.object({ page: z.coerce.number().default(1) }) })
  list(ctx: HttpContext) { ... }
}

Supported schemas: body, query, params. Any Standard Schema v1-compatible library works — Zod, Valibot, ArkType, Effect Schema, etc.

By default the parsed/coerced value replaces the original (transform: true). To disable:

@validate({ body: schema }, { transform: false })

ValidationPipe

ValidationPipe is a standalone pipe for single-value validation:

import { ValidationPipe } from '@stitchem/http';

const emailPipe = new ValidationPipe(z.string().email());

@usePipes(emailPipe)
@get('/verify')
verify(ctx: HttpContext) { ... }

Exception Filters

Exception filters catch errors thrown anywhere in the pipeline and convert them to HTTP responses.

Class Filters

Implement ExceptionFilter and decorate with @catches():

import { catches, ExceptionFilter, HttpContext, HttpException } from '@stitchem/http';

@catches(HttpException)
class HttpExceptionFilter implements ExceptionFilter {
  catch(error: HttpException, ctx: HttpContext): Response {
    return ctx.json({ error: error.message }, error.status);
  }
}

@catches() // catches everything — useful for logging
class LoggingFilter implements ExceptionFilter {
  catch(error: unknown, ctx: HttpContext): void {
    console.error('[unhandled]', error);
    // returning void → error falls through to next filter
  }
}

Apply with @useFilters():

@router('/users')
@useFilters(HttpExceptionFilter)         // all routes
class UserRouter {
  @post('/')
  @useFilters(ValidationFilter)          // this route only
  create(ctx: HttpContext) { ... }
}

Global Filters

app.useGlobal({ filters: [HttpExceptionFilter] });

Filter Chain

Filter priority order: method → class → global. The first filter that returns a Response wins. If a filter returns void, the error falls through to the next filter. If no filter handles it, a 500 response is sent.

Exception filters are always resolved globally (ignoring the strict option).


Exceptions

Built-in exceptions extend HttpException:

throw new BadRequestException('Invalid email');
throw new NotFoundException();
throw new HttpException(418, "I'm a teapot");

| Exception | Status | Default message | |-----------|--------|-----------------| | BadRequestException | 400 | 'Bad Request' | | UnauthorizedException | 401 | 'Unauthorized' | | ForbiddenException | 403 | 'Forbidden' | | NotFoundException | 404 | 'Not Found' | | MethodNotAllowedException | 405 | 'Method Not Allowed' | | ConflictException | 409 | 'Conflict' | | UnprocessableEntityException | 422 | 'Unprocessable Entity' | | TooManyRequestsException | 429 | 'Too Many Requests' | | InternalServerErrorException | 500 | 'Internal Server Error' | | ServiceUnavailableException | 503 | 'Service Unavailable' | | ValidationException | 400 | 'Validation failed' |

ValidationException extends BadRequestException and carries a structured errors object:

{
  body: { email: ['Invalid email format'] },
  query: { page: ['Required'] },
}

Serialization

Use @dto(), @expose(), and @exclude() to declare response shapes, then attach them to route methods with @serialize().

import { dto, expose, exclude, serialize } from '@stitchem/http';

@dto()
class UserResponse {
  @expose() id!: number;
  @expose() name!: string;
  @expose() email!: string;
  // password is not exposed — omitted from output
  password!: string;
}

@router('/users')
class UserRouter {
  @get('/:id')
  @serialize(UserResponse)
  async getUser(ctx: HttpContext) {
    return await this.userService.findById(ctx.param('id'));
    // return value is filtered through UserResponse before ctx.json()
  }
}

Expose mode (one or more @expose() on the class): only explicitly exposed fields appear.

Exclude mode (no @expose(), only @exclude()): all own properties appear except excluded ones.

Computed getters work with both decorators:

@dto()
class UserResponse {
  @expose() id!: number;
  @expose() name!: string;
  @expose() get fullName() { return `${this.firstName} ${this.lastName}`; }
}

Response return values bypass serialization entirely.


HttpAdapter

HttpAdapter is the interface adapters implement to bridge a native framework into @stitchem/http:

export interface HttpAdapter<TApp = unknown> {
  readonly name: string;
  get(): TApp;
  createContext(nativeContext: unknown): HttpContext;
  registerRoute(method: HttpMethod, path: string, handler: RouteHandler): void;
  useMiddleware(handler: MiddlewareHandler): void;
  useErrorHandler(handler: ErrorHandler): void;
  listen(port: number, hostname?: string): Promise<void>;
  close(): Promise<void>;
}

Adapters bridge stitchem's portable HttpContext to the native framework's request/response model.


API Reference

Decorators

| Decorator | Target | Description | |-----------|--------|-------------| | @router(path?) | Class | Marks a class as a router with a base path (default '/') | | @get(path?) | Method | GET route | | @post(path?) | Method | POST route | | @put(path?) | Method | PUT route | | @patch(path?) | Method | PATCH route | | @del(path?) | Method | DELETE route | | @options(path?) | Method | OPTIONS route | | @head(path?) | Method | HEAD route | | @middleware(options?) | Class | Marks a class as middleware. { strict? } controls DI scope | | @useMiddlewares(...targets) | Class / Method | Applies middleware to a router or route | | @guard(options?) | Class | Marks a class as a guard. { strict? } controls DI scope | | @useGuards(...targets) | Class / Method | Applies guards to a router or route | | @usePipes(...pipes) | Class / Method | Applies pipes to a router or route | | @validate(schemas, options?) | Method | Attaches Standard Schema validation | | @catches(...types) | Class | Marks a class as an exception filter | | @useFilters(...filters) | Class / Method | Applies exception filters to a router or route | | @dto() | Class | Marks a class as a DTO for serialization | | @expose() | Field / Getter | Includes this field in serialized output | | @exclude() | Field / Getter | Excludes this field from serialized output | | @serialize(dtoClass) | Method | Applies DTO serialization to a route's return value |

Interfaces

| Interface | Description | |-----------|-------------| | Middleware | use(ctx, next): void \| Promise<void> | | Guard | canActivate(ctx): boolean \| Promise<boolean> | | Pipe | transform(ctx): void \| ResponseTransform \| Promise<...> | | ExceptionFilter | catch(error, ctx): Response \| void \| Promise<...> | | HttpModule | middleware(consumer): void — module-level middleware routing | | HttpAdapter | Adapter contract for bridging native frameworks | | HttpContext | Portable request/response context | | HttpContextStore | Augmentable typed store for per-request data |

Functions

| Function | Description | |----------|-------------| | defineMiddleware(fn) | Brands a function as a stitchem middleware handler |

License

MIT