Skip to Content
ControllerCallable handlers

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:

src/modules/user/UserController.ts
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.

src/app/page.tsx
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.

src/modules/user/UserHandlers.ts
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.

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; }
Last updated on