Skip to Content
Calling Procedures Locally

Calling Procedures Locally

Every procedure created with the procedure function can be used outside an HTTP request context as a regular function. It exposes an fn method that calls the handler directly, partially simulating the signature of an RPC method.

Let’s say you have the following controller class:

src/modules/user/UserController.ts
import { z } from 'zod'; import { procedure, prefix, get, operation } from 'vovk'; import UserService from './UserService'; @prefix('users') export default class UserController { @operation({ summary: 'Update user', description: 'Update user by ID with Zod validation', }) @get('{id}') static getUser = procedure({ params: z.object({ id: z.string().uuid(), }), async handle(req) { // ... }, }); }

When it’s compiled to an RPC module, the HTTP request can be performed like this:

import { UserRPC } from 'vovk-client'; const user = await UserRPC.getUser({ params: { id: '69' }, disableClientValidation: true, // disables client-side validation meta: { hello: 'world' }, // available as xMetaHeader metadata }); console.log('User:', user);

But the fn method allows you to call the procedures directly, in the current evaluation context, without performing an HTTP request:

import UserController from '@/modules/user/UserController'; const user = await UserController.getUser.fn({ params: { id: '69' }, disableClientValidation: true, // disables validation meta: { hello: 'world' }, // available as root metadata }); console.log('User:', user);

This will invoke UserController.getUser like a normal function, performing validation before executing the handler.

There are several core use cases for local procedures:

For SSR, SSG, PPR, etc.: You can use the method in a server component.

src/app/user/page.tsx
import UserController from '@/modules/user/UserController'; export default async function UserPage() { const user = await UserController.getUser.fn({ params: { id: '69' }, }); return ( <p>User: {JSON.stringify(user)}</p> ); }

For AI tool execution: controllers that define procedures with procedure() can be passed as a “module” to the deriveTools utility to call procedures in the current context without performing HTTP requests.

import { deriveTools } from 'vovk'; const { tools } = deriveTools({ modules: { UserController }, }); console.log(tools); // [{ name, description, parameters, execute }, ...]

Rules of Locally Called Procedures

fn functions do not imitate the Request object but partially implement the VovkRequest interface by exposing only the custom vovk property with async vovk.body(), vovk.query(), vovk.params(), and vovk.meta(). In other words, properties such as req.url or req.headers are not defined in the local procedure context. A suitable req signature looks like Pick<VovkRequest<TBody, TQuery, TParams>, 'vovk'>.

To ensure that the implemented HTTP handler can also be called with fn, you can use request object destructuring when defining the handler to use only the vovk property:

export default class UserController { @post('{id}') static updateUser = procedure({ body: z.object({ /* ... */ }), params: z.object({ /* ... */ }), query: z.object({ /* ... */ }), output: z.object({ /* ... */ }), async handle({ vovk }) { const body = await vovk.body(); const query = vovk.query(); const params = vovk.params(); const meta = vovk.meta<{ hello: string }>(); // ... }, }); }

Local procedure is decorated with the same decorators as the HTTP handler. If you create a decorator to wrap an HTTP handler for use with fn, avoid using NextRequest‑specific properties like req.headers, or req.nextUrl.

import { createDecorator } from 'vovk'; const myDecorator = createDecorator(({ vovk }, next) => { const meta = vovk.meta<{ foo: string }>(); // ... return next(); });

You can detect if the procedure is called locally by checking req.url, which is undefined when called via fn:

import { createDecorator } from 'vovk'; const myDecorator = createDecorator((req, next) => { if (typeof req.url === 'undefined') { console.log('Local context'); } else { console.log('HTTP request context'); } // ... return next(); });

Even though req is not a Request, you can still use Next.js features like headers or cookies from next/headers in the local procedure context:

import { createDecorator } from 'vovk'; import { headers } from 'next/headers'; const myDecorator = createDecorator(async ({ vovk }, next) => { const reqHeaders = await headers(); // ... return next(); });

An HTTP decorator such as @get, @post, etc., is not required for a local procedure to work. The only requirement is to use the procedure function to create the procedure.

src/modules/user/UserHandlers.ts
import { z } from 'zod'; import { procedure } from 'vovk'; export default class UserHandlers { static updateUser = procedure({ body: z.object({ /* ... */ }), // ... }); } UserHandlers.updateUser.fn({ /* ... */ });

When a static class is implemented this way, it behaves like a “validated service”, which can be attached to the controller later or used as a standalone collection of validated functions.

src/modules/user/UserController.ts
import { prefix, post } from 'vovk'; import UserHandlers from './UserHandlers'; @prefix('users') export default class UserController { @post('{id}') static updateUser = UserHandlers.updateUser.bind(UserHandlers); }
Last updated on