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

@theokit/http

v2.3.0

Published

NestJS-style decorators (@Controller, @Get, @Post, @Body, @UseGuards) over Web Standards, compiling to TheoKit route registrations.

Readme

@theokit/http

NestJS-style decorators (@Controller, @Get, @Post, @Body, @UseGuards) over Web Standards — Request and Response, not node:http. They compile down to the same route registrations theokit serves, so a decorator controller and a route() builder are two authoring surfaces over one runtime.

Renamed. This package was published as @theokit/http-decorators until 0.3.0. That name is frozen at the old surface — install @theokit/http.

Install

pnpm add @theokit/http reflect-metadata

The decorators are the TypeScript legacy kind, so the consuming project needs:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

emitDecoratorMetadata is only required for the DTO-class form of @Body (below). The preferred form — @Body(schema) — works without it.

Quick start

import { Controller, Get, Post, Body, Param, Public } from '@theokit/http'
import { z } from 'zod'

// Convention: @Controller() on CatsController infers the prefix "api/cats".
// Pass a string to override: @Controller('api/v2/cats').
//
// `@Public()` is the access decision "anyone may call this". Every route needs one — a guard or
// this — because a route that declares neither is refused with 403 rather than served. See
// "Every route declares who may call it" below.
@Public()
@Controller()
export class CatsController {
  @Get()
  findAll() {
    return { cats: [] }
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return { id }
  }

  @Post()
  create(@Body(z.object({ name: z.string(), age: z.number().min(0) })) body: unknown) {
    return { created: body }
  }
}

Every route declares who may call it

A route says one of two things, and saying neither is not a third option:

| | how it is said | |---|---| | anyone may call it | @Public() — on the method, or on the class to cover every route under it | | someone decides | @UseGuards(SomeGuard) — likewise on either |

A route that declares neither is refused with 403 at dispatch, in every dispatcher this package ships, and theokit build fails on it before that. The reason is that guards: [] used to mean both "open on purpose" and "nobody said", so the dispatcher took the permissive reading — and a route nobody thought about is the one that ships open.

undeclaredRoutes: 'warn' restores the old behaviour while you migrate, with one warning per route:

TheoApp.create({ controllers, undeclaredRoutes: 'warn' }) // also on createDecoratorHandler(...)

Guards still run on a @Public() route — the decorator answers who may call it, not what else happens on the way in. For "any signed-in caller", theokit/server/auth exports the guard rather than leaving every app to write it:

import { createSessionManagerWeb, Authenticated } from 'theokit/server/auth'

const sessions = createSessionManagerWeb<{ userId: string }>({ secret: process.env.SESSION_SECRET! })

@Controller('api/tasks')
@UseGuards(Authenticated(sessions))
export class TasksController { … }

Validation — Zod is the single source of truth

Pass the schema to @Body directly. The bridge validates the request against it and feeds the same schema to the OpenAPI emitter and the typed-client codegen:

@Post()
create(@Body(z.object({ name: z.string().min(2), breed: z.string() })) body: unknown) { … }

A DTO class works too, when a shared, named shape reads better. Attach the schema as a static; this form needs emitDecoratorMetadata, because the bridge finds the class through design:paramtypes:

class CreateCatDto {
  static schema = z.object({ name: z.string(), age: z.number() })
}

@Post()
create(@Body() body: CreateCatDto) { … }

When neither is present the body is passed through raw and the bridge warns — validation is never silently skipped.

Pipeline

middleware → guards → interceptors → handler, with filters catching what escapes.

import { Controller, Get, UseGuards, type CanActivate, type ExecutionContext } from '@theokit/http'

class AuthGuard implements CanActivate {
  canActivate(ctx: ExecutionContext): boolean {
    return ctx.getRequest().headers.get('authorization') !== null
  }
}

@Controller('api/admin')
export class AdminController {
  @UseGuards(AuthGuard)
  @Get()
  dashboard() {
    return { status: 'authenticated' }
  }
}

A guard returning false produces 403 Forbidden (ForbiddenException); throw UnauthorizedException from the guard when 401 is what you mean. Class-level guards run before method-level ones.

An interceptor is intercept(request, next) — Web Standard Request in, and next() wrapping the handler call only (the body is already parsed by then). Not calling next() short-circuits the handler; calling it twice is memoized to one execution.

An exception filter is catch(exception, host) returning a Response, where host.getRequest() is the request that failed.

Serving an agent from a controller

@Expose binds an agent built in agents/<name>.ts to a controller property, so its route, its streaming and its auth are visible in one place:

import { Controller, Expose, UseGuards } from '@theokit/http'

import supportAgent from '../../agents/support.js'

@Controller('api/agents')
@UseGuards(AuthGuard)
export class AgentsController {
  @Expose(supportAgent)
  support!: unknown // → POST /api/agents/support, behind AuthGuard
}

The request is delegated straight to the one agent runtime. Guards run on that route; interceptors do not.

Decorators reference

| Decorator | Kind | Purpose | |---|---|---| | @Controller(prefix?, opts?) | Class | Route prefix scope (inferred from the class name when omitted) | | @Get/@Post/@Put/@Patch/@Delete/@Options/@Head/@All(path?) | Method | HTTP verb endpoints | | @Body(schemaOrKey?) | Parameter | Request body — Zod schema, DTO class, or body[key] | | @Param(key?) | Parameter | Route params (or params[key]) | | @Query(key?) | Parameter | Query string (or query[key]) | | @Headers(name?) | Parameter | Request headers | | @Req() / @Res(opts?) | Parameter | The raw Request / response (passthrough option) | | @Session() / @Ip() / @HostParam(key?) | Parameter | Session, client IP, host params | | @HttpCode(status) | Method | Override the response status | | @Header(name, value) | Method | Set a response header | | @Redirect(url, status?) | Method | Redirect response | | @UseGuards(...) | Class/Method | Attach guards | | @UseInterceptors(...) | Class/Method | Attach interceptors | | @UseFilters(...) / @Catch(...) | Class/Method | Exception filters | | @Throttle(opts) / @SkipThrottle() | Class/Method | Rate-limit policy | | @SetMetadata(key, value) / createDecorator<T>() | Class/Method | Custom metadata, read back with Reflector | | @Expose(agent, opts?) | Property | Serve a built agent from this controller |

Build your own policy decorator with createDecorator:

const Roles = createDecorator<string[]>()
// @Roles(['admin']) → read it in a guard with reflector.get(Roles, handler)

HttpStatus ships the 30 status codes the framework uses, alongside an exception class per code (BadRequestException, ConflictException, TooManyRequestsException, …).

Subpaths

| Subpath | What lives there | |---|---| | . | Decorators, metadata, the bridge, exceptions, TheoApp, createTypedClient, contract, static serving | | ./theokit-plugin | The plugin that wires controllers into a TheoKit app | | ./app | App-level composition | | ./runtime/node | The Node runtime adapter | | ./action-encryption, ./server-inserted-html, ./css-resource | Opt-in capabilities kept off the main bundle |

registerControllers (low-level)

For advanced use without the Vite plugin:

import { registerControllers } from '@theokit/http'

const routes = registerControllers([CatsController])
// RouteRegistration[] — verb, fullPath, and the metadata walk per method

Limitations

  • Singleton-scope controllers only. A controller is instantiated once, when the handler is built — NestJS request-scoped controllers (@Injectable({ scope: Scope.REQUEST })) have no equivalent here. Guards and interceptors are the opposite: a fresh instance per request, unless a DI container resolves them.
  • No response object before the handler returns. ExecutionContext exposes the request, the URL, the controller class and the handler name — there is nothing to mutate headers on until a Response exists. Set them with @Header, or return a Response yourself.
  • A handler returning a Response owns it. @HttpCode and @Header are not applied on that path — the same behaviour as NestJS with @Res({ passthrough: false }).

Troubleshooting

HttpDecoratorsConfigError: emitDecoratorMetadata not enabled — add both compiler flags shown under Install, or pass the schema inline as @Body(schema), which does not need them.

HttpDecoratorsConfigError: missing @Controller() decorator — a class has @Get/@Post methods but no @Controller().

Bundle cost

~8-13KB gzipped for consumers who opt in (reflect-metadata ~3KB plus this package); 0KB for those who do not.

Licence

Apache-2.0 — see LICENSE.