Local Procedures
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:
import { z } from 'zod';
import { procedure, prefix, post, 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',
})
@post('{id}')
static updateUser = procedure({
body: z
.object({
name: z.string().describe('User full name'),
age: z.number().min(0).max(120).describe('User age'),
email: z.email().describe('User email'),
})
.describe('User object'),
params: z.object({
id: z.uuid().describe('User ID'),
}),
output: z
.object({
success: z.boolean().describe('Success status'),
id: z.uuid().describe('User ID'),
})
.describe('Response object'),
async handle(req) {
const body = await req.vovk.body();
const params = req.vovk.params();
const meta = req.vovk.meta<{ hello: string; xMetaHeader: { hello: string } }>();
// ...
},
});
}When itβs compiled to an RPC module, the HTTP request can be performed like this:
import { UserRPC } from 'vovk-client';
const response = await UserRPC.updateUser({
params: { id: '69' },
body: {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com',
},
disableClientValidation: true, // disables client-side validation
meta: { hello: 'world' }, // available as xMetaHeader metadata
});
console.log('Response:', response);But the fn method allows you to call the controller method directly, in the current evaluation context, without performing an HTTP request:
import UserController from '@/modules/user/UserController';
const response = await UserController.getUser.fn({
params: { id: '69' },
body: {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com',
},
disableClientValidation: true, // disables client-side validation
meta: { hello: 'world' }, // available as root metadata
});
console.log('Response:', response);This will invoke UserController.updateUser 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 as a server-side function, allowing you to call it from server-side code without performing an HTTP request.
import UserController from '@/modules/user/UserController';
export default async function UserPage() {
const response = await UserController.getUser.fn({
params: { id: '69' },
});
return (
<p>Response: {JSON.stringify(response)}</p>
);
}For LLM function calling: Each controller can be used as a βmoduleβ for LLM function calling, implemented with the deriveTools utility to call controller methods 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 Local Procedures
fn functions do not imitate the Request object but partially implement the VovkRequest object 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 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.
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.
import { prefix, post } from 'vovk';
import UserHandlers from './UserHandlers';
@prefix('users')
export default class UserController {
@post('{id}')
static updateUser = UserHandlers.updateUser.bind(UserHandlers);
}