Skip to main content
skill CX Codex

decorators — Claude Code skill: what it does, how to install

Teaches coding agents to use built-in Vovk.ts route decorators, build custom middleware with createDecorator, and handle request metadata safely.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add finom/vovk --skill decorators -a claude-code
Open on GitHub

What decorators does

This skill guides AI agents through Vovk.ts controller decorators. It covers HTTP route markers like @get and @post, kebab-case path generation with .auto(), and class-level route prefixes via @prefix.

Custom middleware runs through createDecorator to intercept requests before and after execution. Handlers exchange state using req.vovk.meta(). The skill enforces explicit generic types on every meta() call to avoid falling back to an untyped Record<string, any>.

Next.js 15 projects with Webpack require experimentalDecorators enabled in tsconfig.json to compile successfully. Turbopack handles them natively. Procedure implementation details and full OpenAPI generation are intentionally excluded, as they belong to dedicated skills.

What it helps with

  • build custom authorization guards using createDecorator
  • prefix routes across an entire controller class
  • share typed user context across handlers with req.vovk.meta
  • configure route-level CORS and response headers

Example prompts for the agent

  • Create an auth guard decorator for Vovk.ts using createDecorator
  • Set up a users prefix for this controller and generate kebab-case routes with .auto()
  • Write a role check decorator that reads metadata from req.vovk.meta

How to install decorators

1Skills CLI (Vercel)

npx skills add finom/vovk --skill decorators -a claude-code

2Manually: put the files in place

~/.claude/skills/decorators/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/decorators/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about decorators

How do I install decorators in Claude Code?

Run npx skills add finom/vovk --skill decorators -a claude-code. This installs the skill file to ~/.claude/skills/decorators/SKILL.md.

What does the decorators skill do?

It provides rules for writing Vovk.ts decorators, ordering middleware execution, and passing metadata safely between decorators.

Why does req.vovk.meta require an explicit generic?

Its default type is Record<string, any>. Providing a generic type alias prevents silent loss of TypeScript inference between middleware and route handlers.

Source text

SKILL.md · MIT
---
name: decorators
description: Vovk.ts decorators — built-in (`@prefix`, `@operation`, `@get/@post/@put/@patch/@del`, `.auto()`) and custom via `createDecorator`. Covers authorization / auth decorators, middleware-style wrapping (pre-handler + post-handler logic), `req.vovk.meta()` for cross-decorator state, stacking order, and the local (`.fn()`) vs HTTP context distinction. Use whenever the user asks to "write a custom decorator", "add auth middleware", "protect an endpoint", "build an authorization decorator", "createDecorator", "stack decorators", "share data between decorator and handler", "prefix all routes", "@auto", "decorator execution order". Does NOT cover procedure authoring → hand off to `procedure` skill. Does NOT cover `@operation` for OpenAPI beyond auth-related fields → hand off to `openapi` skill. Does NOT cover `@operation.x-tool` for tool derivation → hand off to `tools` skill.
---

# Vovk.ts decorators

Decorators wrap static methods on controllers. Three kinds:

- **HTTP decorators** (`@get`, `@post`, `@put`, `@patch`, `@del`) — mark procedure as HTTP route. See `procedure` skill.
- **Metadata decorators** (`@prefix`, `@operation`) — attach info, no behavior change.
- **Custom decorators** (`createDecorator`) — middleware wrapping handler. How auth, logging, feature flags attach.

One syntax: `@decorator`. `vovk init` enables `experimentalDecorators` in `tsconfig.json`; a webpack build (`next build --webpack`, Next.js 15's default) needs it, Turbopack (Next.js 16's default) compiles vovk's decorators either way.

## `@prefix`

Class-level route prefix:

```ts
import { prefix, get, procedure } from 'vovk';

@prefix('users')
export default class UserController {
  @get('{id}')
  static getUser = procedure({ /* ... */ }).handle(/* ... */);
}
// → GET /api/users/{id}
```

Root-level — `@prefix('')` or omit (same effect):

```ts
export default class RootController {
  @get()
  static ping = procedure().handle(() => ({ ok: true }));
}
// → GET /api
```

## `.auto()` — name-derived routes

Use method name instead of path:

```ts
@put.auto()
static doSomething = procedure().handle(/* ... */);
// → PUT /api/<prefix>/do-something
```

Method names → kebab-case. Works with every HTTP decorator (`@get.auto()`, `@post.auto()`, etc.), same options.

## HTTP decorator options

Second arg: response-header + CORS config.

```ts
@put('do-something', {
  headers: { 'x-hello': 'world' },
  cors: true,
})
static doSomething = procedure().handle(/* ... */);
```

- `headers` — merged into response.
- `cors: true` — permissive CORS for that route (`access-control-allow-origin: *`, preflight answered). Needs `OPTIONS` in segment's `initSegment()` exports — else Next.js answers the preflight itself (204 with `Allow`, no `Access-Control-*` headers), and the browser blocks the cross-origin call. Exact headers and CORS for every route → **`base`**.

## Custom decorators — `createDecorator`

Middleware-shaped: receive request + `next`, work before/after, return response. `next()` returns the handler's value (after the `output` check) or throws its error (validation errors included). What the decorator returns is the response: a value is sent as JSON with 200, a `Response` as it is — the way to set a status other than 200 or answer errors in another format (recipes in **`base`**).

```ts
import { createDecorator } from 'vovk';

const timingDecorator = createDecorator(async (req, next) => {
  const start = Date.now();
  const response = await next();
  console.log(`[${req.method}] ${req.url} — ${Date.now() - start}ms`);
  return response;
});
```

Stack it with the HTTP decorator (order below):

```ts
@get('{id}')
@timingDecorator()
static getUser = procedure({ /* ... */ }).handle(/* ... */);
```

### Auth decorator pattern

Gate access, pass user state to handler via `req.vovk.meta()`. **Always pass explicit generic to every `meta()` call** — both set + read — and share single type alias across every site touching auth metadata. `meta()`'s generic defaults to `Record<string, any>` → untyped call silently loses inference, sides drift apart.

```ts
// auth.ts — define once, import everywhere the auth decorator is used
import { createDecorator, HttpException, HttpStatus } from 'vovk';
import { headers } from 'next/headers';

export type AuthMeta = { user: User };

export const authGuard = createDecorator(async (req, next) => {
  const token = (await headers()).get('authorization');
  if (!token) {
    throw new HttpException(HttpStatus.UNAUTHORIZED, 'Missing token');
  }
  const user = await parseToken(token); // your auth logic — returns `User | null`
  if (!user) {
    throw new HttpException(HttpStatus.UNAUTHORIZED, 'Invalid or expired token');
  }
  req.vovk.meta<AuthMeta>({ user });
  return next();
});
```

Reading the token via `next/headers` (not `req.headers`) makes the guard work over HTTP and under `.fn()`. See **Local vs HTTP context** below.

Consume in handler — same `AuthMeta`:

```ts
import { authGuard, type AuthMeta } from './auth';

@get('{id}')
@authGuard()
static getUser = procedure({ /* ... */ }).handle((req) => {
  const { user } = req.vovk.meta<AuthMeta>();
  // user is `User`, present and typed
});
```

**Throw, don't return**: `HttpException` short-circuits chain. Returning response object bypasses handler silently — possible, bug magnet.

### Role / scope check

Decorators with arguments don't wrap `createDecorator` in outer factory — handler signature is `(req, next, ...args)`, so call-site args (`@roleGuard('admin')`) flow through to handler's trailing params. Declare once, reuse same `AuthMeta`:

```ts
import { authGuard, type AuthMeta } from './auth';

const roleGuard = createDecorator(async (req, next, role: 'admin' | 'user') => {
  const { user } = req.vovk.meta<AuthMeta>();
  if (user.role !== role) {
    throw new HttpException(HttpStatus.FORBIDDEN, 'Insufficient role');
  }
  return next();
});

@get('admin-only')
@authGuard()
@roleGuard('admin')
static adminOnly = procedure().handle(/* ... */);
```

`TArgs` inferred from handler signature → `@roleGuard('admin')` type-checked against `'admin' | 'user'` at call site.

`@authGuard()` is **outermost** wrapping decorator (topmost, after `@get`) → runs first, populates `meta<AuthMeta>({ user })`. `@roleGuard('admin')` runs next, reads same alias, decides whether to throw. No need to guard `if (!user)` — auth threw `UNAUTHORIZED` otherwise. See stacking-order section below.

## `req.vovk.meta()` — cross-decorator state

Key/value store on request. Signature: `meta<T>(value?: T | null): T`, `T` defaults to `Record<string, any>` — **always pass generic** on set + read so TypeScript checks shape, doesn't fall through to `any`. For concern used by more than one decorator (auth, tracing, feature flags), hoist type to shared module + import everywhere:

```ts
type TraceMeta = { traceId: string };

req.vovk.meta<AuthMeta>({ user });                   // set, typed against shared alias
const { user } = req.vovk.meta<AuthMeta>();          // read, same alias
req.vovk.meta<TraceMeta>({ traceId: 'abc' });        // merge (keeps existing `user`)
req.vovk.meta(null);                                 // clear all (no generic needed)
```

Calls **merge** — consecutive `meta()` accumulate. How decorators pass state to each other + handler. Each `meta()` types only its slice; merged object carries keys from every contributor.

### Client-supplied metadata (`x-meta`)

Clients ship metadata via `x-meta` header. Lives under separate key (`xMetaHeader`) in `meta()` — does **not** overwrite server-set keys. Server-trusted state safe from client spoofing.

## Stacking order

Decorators execute **top-to-bottom** — topmost wrapping decorator (furthest from handler) runs first pre-handler. Opposite of TS's bottom-up *application* order: `@C` applied first → ends up innermost wrapper, runtime runs *last* pre-handler.

```ts
@get('/x')
@decoratorA()   // runs FIRST (outermost wrapper)
@decoratorB()   // runs second
@decoratorC()   // runs LAST pre-handler (innermost, closest to handler)
static handler = procedure().handle(/* ... */);
```

Pre-handler: A → B → C → handler. Post-handler (after `next()` resolves): handler → C → B → A.

Auth: put authentication **at top** (outermost) → populates `meta()` before downstream reads. Authorization / role checks stack just below — see what auth wrote.

### Wrapping decorators vs. schema-only decorators

Not every decorator uses `req`. Placement rule depends on what decorator does:

- **Wrapping decorators** — read `req` / call `next()` (auth, logging, timing, any `createDecorator(handler, …)` with non-null handler). Run on HTTP and `.fn()` calls on either side of `@get`/`@post`/etc.: dispatcher calls latest wrapper, so one applied after HTTP decorator (written above it) still sees `req`. Order stays top-to-bottom. Convention: stack them **below** HTTP decorator.
- **Schema-only decorators** — `@operation`, anything written as `createDecorator(null, initHandler)`. Only mutate handler schema (OpenAPI, tool derivation, etc.), pass through at runtime. Placement doesn't change behavior; convention puts them **on top** (above `@get`) for readability — metadata reads naturally before HTTP verb line.

```ts
@operation({ summary: 'List users' })  // schema-only — on top, reads like a doc comment
@get('/users')                          // HTTP decorator
@authGuard()                            // wrapping — below @get by convention
static listUsers = procedure().handle(/* ... */);
```

## Local vs HTTP context

When procedure called via `.fn()` (SSR, server components, tests), `req.url` is `undefined`. Decorators run in both modes — detect + branch when logic depends on real HTTP:

```ts
const myDecorator = createDecorator((req, next) => {
  if (typeof req.url === 'undefined') {
    // Local context — no network, no real headers
  } else {
    // HTTP request
  }
  return next();
});
```

For headers/cookies, prefer imported `headers()` / `cookies()` from `next/headers` over `req.headers` / `req.cookies`. `next/headers` works over HTTP and under `.fn()` inside a Next.js request — no branch, no reliance on caller synthesizing `req`. Outside a request (unit test, script) it throws "`headers` was called outside a request scope"; mock it there, e.g. `vi.mock('next/headers', () => ({ headers: async () => new Headers({ authorization: 'Bearer test' }) }))`:

```ts
import { createDecorator } from 'vovk';
import { headers } from 'next/headers';

const authGuard = createDecorator(async (req, next) => {
  const token = (await headers()).get('authorization');
  // ...
  return next();
});
```

A `req.headers` / `req.cookies` read works in HTTP mode only — under `.fn()` both are `undefined` (`.fn()` builds only `req.vovk`, and its options take no headers). `next/headers` sidesteps that.

## Flows

### "Protect an endpoint with bearer auth"

1. Define `type AuthMeta = { user: User }` in shared module.
2. Write `authGuard` reading `authorization`, validating, calling `req.vovk.meta<AuthMeta>({ user })`.
3. Stack at top (outermost): `@get('x') @authGuard() static ...`.
4. Read `req.vovk.meta<AuthMeta>()` inside handler — same alias.

### "Only admins can hit this route"

Stack `@authGuard() @roleGuard('admin')` — auth at top (runs first, sets `user`), role check below (runs second, reads `user`).

### "Log every request with timing"

Timing decorator wrapping `next()` — record start, await, record end. Apply to class via shared constant + `@` on every method, or lift to Next.js middleware if every route needs it.

### "Share state between two decorators"

Define shared type (`type SharedMeta = { user: User }`). Decorator 1: `req.vovk.meta<SharedMeta>({ user })`. Decorator 2: `const { user } = req.vovk.meta<SharedMeta>()`. Same alias both sides — `meta()` merges, TypeScript stays in sync.

### "Skip a decorator when called locally"

Branch on `typeof req.url === 'undefined'` inside decorator body, or don't stack the decorator on procedures called via `.fn()`.

**Safety caveat — only skip auth/authorization when caller already did equivalent check.** Server Component with authenticated session calling `.fn()` purely to fetch already-authorized data is legitimate skip. `.fn()` against unauthenticated context (cron jobs, background work, untrusted callers) still needs to authenticate — keep guard + ensure it can read headers (via `next/headers`, which needs a Next.js request: a cron route handler has one, a standalone script doesn't), or do equivalent explicit check before calling `.fn()`.

If you skip guard for one callsite, audit every other `.fn()` callsite for same procedure. Easy to add second call later that assumes "this procedure is protected" + quietly isn't. When in doubt, keep the guard: inside a Next.js request it reads the same headers over HTTP and under `.fn()`.

## Gotchas

- **Stacking order top-down.** Topmost wrapping decorator runs first pre-handler; innermost (closest to handler) runs last. Auth at top; authorization / role checks below; logging/tracing above everything to see whole timing.
- **`meta()` merges, doesn't replace.** Pass `null` to clear. Multiple decorators setting different keys all land in final object.
- **Client `x-meta` sandboxed.** Under `xMetaHeader` in `meta()` — server-trusted state stays safe. Don't collapse together.
- **Throw to short-circuit.** `throw new HttpException(...)` is standard control-flow for auth failures. Returning response works but unusual + easy to misread.
- **Local context has no `req.url`.** If decorator dereferences HTTP-specific fields, guard. `next/headers` works in both, inside a Next.js request; in a unit test, mock it.
- **Decorators on `.fn()` still run.** If you don't want auth in SSR, either don't stack decorator on that procedure or branch inside body. Controller-only procedures (no HTTP decorator) can still have custom decorators — run on every `.fn()` call.
- **Vitest drops decorators** unless `useDefineForClassFields` is on → `procedure` skill, Testing.
- **`.auto()` names flow from method names.** Renaming method renames route — breaking change for consumers. Prefer explicit paths on public APIs.

© finom · MIT · finom/vovk

Similar skills

All skills for Backend and APIs →

From the same set15

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor