Callable handlers
Every controller method created with withValidationLibrary function, that works behind the scenes of all validation libraries, can be used outside of HTTP request context as a regular function. It provides fn
method that can be used to call the method directly, simulating signature of a resulting RPC module function.
Letβs say you have the following controller class:
import { z } from 'zod';
import { prefix, post, openapi } from 'vovk';
import { withZod } from 'vovk-zod';
import UserService from './UserService';
@prefix('users')
export default class UserController {
@openapi({
summary: 'Update user',
description: 'Update user by ID with Zod validation',
})
@post('{id}')
static updateUser = withZod({
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'),
}),
query: z.object({
notify: z.enum(['email', 'push', 'none']).describe('Notification type'),
}),
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 query = req.vovk.query();
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',
},
query: {
notify: 'email',
},
disableClientValidation: true, // disables client-side JSON validation
meta: { hello: 'world' }, // available as xMetaHeader metadata
});
console.log('Response:', response);
But the fn
method allows you to call the controller method directly:
import UserController from '@/modules/user/UserController';
const response = await UserController.updateUser.fn({
params: { id: '69' },
body: {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com',
},
query: {
notify: 'email',
},
disableClientValidation: true, // disables Zod validation
meta: { hello: 'world' }, // available as root metadata
});
console.log('Response:', response);
This will invoke UserController.updateUser
method just like a normal function, performing validation with Zod (or other library) before executing the handler and imitating the same signature as the RPC module function.
There are several core use cases for callable handlers:
For SSR, SSG, PPR etc: You can use the method as a server-side function, allowing you to call it from the server-side code without performing an HTTP request.
import UserController from '@/modules/user/UserController';
export default async function Page() {
const response = await UserController.updateUser.fn({
params: { id: '69' },
body: {
name: 'John Doe',
age: 30,
email: 'john.doe@example.com',
},
query: {
notify: 'email',
},
});
console.log('Response:', response);
return (
<div>
<h1>User Updated</h1>
<p>Response: {JSON.stringify(response)}</p>
</div>
);
}
For LLM Function Calling: Each controller can be used as a βmoduleβ for LLM Function Calling, implemented with createLLMTools
utility. This allows you to call the controller method as a function from AI models, providing a structured way to interact with your application.
import { createLLMTools } from 'vovk';
const { tools } = createLLMTools({
modules: { UserController },
});
console.log(tools); // [{ name, description, parameters, execute }, ...]
Rules of callable handlers
fn
functions do not imitate the Request
object, but partially simulates VovkRequest object, providing only the custom vovk
property with methods to access input data, such as async vovk.body()
, vovk.query()
, vovk.params()
, and vovk.meta()
. In other words, req
properties such as req.url
or req.headers
are not defined in the callable handler context. A proper signature of the req
object looks like Pick<VovkRequest<BODY, QUERY, PARAMS>, 'vovk'>
. In order to make sure that the implemented HTTP handler can be also called with fn
, you can use object destructing when defining the handler:
export default class UserController {
@post('{id}')
static updateUser = withZod({
body: z.object({
/* ... */
}),
params: z.object({
/* ... */
}),
query: z.object({
/* ... */
}),
output: z.object({
/* ... */
}),
async handle({ vovk }) {
// destructuring req to get "vovk"
const body = await vovk.body();
const query = vovk.query();
const params = vovk.params();
const meta = vovk.meta<{ hello: string }>();
// ...
},
});
}
Another thing to keep in mind is that the callable handler is decorated with the same decorators as the original HTTP handler, so when you create a decorator that is going to wrap the HTTP handler that is going to be callable, you need to make sure that you donβt use req
properties relative to NextRequest
, such as req.url
, req.headers
or req.nextUrl
.
import { createDecorator } from 'vovk';
const myDecorator = createDecorator(({ vovk }, next) => {
const meta = vovk.meta<{ foo: string }>();
// ...
return next();
});
You can check if the handler is called as a callable handler by checking req.url
property, which is undefined
in the callable handler context:
import { createDecorator } from 'vovk';
const myDecorator = createDecorator((req, next) => {
if (typeof req.url === 'undefined') {
console.log('Callable handler context');
} else {
console.log('HTTP request context');
}
// ...
return next();
});
Even though the req
argument isnβt a Request
object, you can still use built-in Next.js features such as headers
or cookies
from next/headers
to access headers and cookies in the callable handler context:
import { createDecorator } from 'vovk';
import { headers } from 'next/headers';
const myDecorator = createDecorator(async ({ vovk }, next) => {
const headers = await headers();
// ...
return next();
});
Good to know
An HTTP decorator such as @get
, @post
etc isnβt required for the callable handler to work, the only requirement is to use withValidationLibrary
function (via withZod
for example) to create the handler.
import { z } from 'zod';
import { withZod } from 'vovk';
export default class UserHandlers {
static updateUser = withZod({
body: z.object({
/* ... */
}),
// ...
});
}
UserHandlers.updateUser.fn({
/* ... */
});
When static class is implemented this way, it behaves like βvalidated serviceβ, or βdetouched HTTP handlers libraryβ, 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;
}