req.vovk Interface
While using built-in NextRequest functions like req.json() and req.nextUrl.searchParams.get() is suffecient for most use cases, Vovk.ts also monkey-patches the request object with vovk property that provides additional methods for more advanced input data handling. It covers the following scenarios:
- Receive data parsed by the validation library (
req.json()andreq.nextUrl.searchParams.get()return data as is), unlesspreferParsedisnβt set tofalse. - Implement nested query parameters parsing.
- Read form data as a typed object, instead of
FormData, provided byreq.formData(). - Implement request metadata storage.
async req.vovk.body()
The req.vovk.body function returns the request body parsed as an object. In most cases, it behaves the same as req.json(), but returns data modified by the validation library (if any).
import { post, type VovkRequest } from 'vovk';
export default class UserController {
@post()
static async createUser(req: VovkRequest<{ foo: string }>) {
const body = await req.vovk.body(); // body type is { foo: string }
// ...
}
}req.vovk.query()
The req.vovk.query function returns typed query parameters, allowing for nested data structures.
import { get, type VovkRequest } from 'vovk';
export default class UserController {
@get()
static async getUser(req: VovkRequest<null, { id: string }>) {
const query = req.vovk.query(); // query type is { id: string }
// ...
}
}Nested data is serialized as a query string with square brackets, commonly referred to as βPHPβstyleβ or βbracket notationβ.
- Square brackets
[ ]denote keys for arrays or nested objects. - Sequential numeric indices (e.g.,
[0],[1]) represent array elements. - Named keys (e.g.,
[f],[u]) represent object properties. - The structure can be nested arbitrarily to represent complex data.
The following query string:
?simple=value&array[0]=first&array[1]=second&object[key]=value&nested[obj][prop]=data&nested[arr][0]=item1&nested[arr][1]=item2&complex[items][0][name]=product&complex[items][0][price]=9.99&complex[items][0][tags][0]=new&complex[items][0][tags][1]=featuredIs parsed as:
{
simple: "value",
array: ["first", "second"],
object: {
key: "value"
},
nested: {
obj: {
prop: "data"
},
arr: ["item1", "item2"]
},
complex: {
items: [
{
name: "product",
price: "9.99",
tags: ["new", "featured"]
}
]
}
}req.vovk.params()
The req.vovk.params function returns typed route parameters. To type it properly, use the third generic argument of VovkRequest (alternative to the second procedure argument).
import { get, type VovkRequest } from 'vovk';
export default class UserController {
@get('{id}')
static async getUser(req: VovkRequest<null, null, { id: string }>) {
const params = req.vovk.params(); // params type is { id: string }
// ...
}
}async req.vovk.form()
The req.vovk.form function returns the request FormData serialized into an object. Itβs a typeβsafe alternative to req.formData() that reads form data and uses Object.fromEntries(formData.entries()) to convert it into a plain object. The generic argument defines the shape of the form data.
interface Data {
foo: string;
bar: string;
file: File;
}
const formDataObject = await req.vovk.form<Data>();
// the same as
// const body = await req.formData();
// const formDataObject = Object.fromEntries(body.entries()) as Data;When procedure function is used with the isForm option, req.vovk.form is typed automaticallyβno generic is required.
req.vovk.meta()
Custom metadata is accessible through the vovk.meta method in the procedure and in custom decorators.
import { createDecorator, get, type VovkRequest } from 'vovk';
const myDecorator = createDecorator(async (req, next) => {
req.vovk.meta({ hello: 'world' }); // set request meta for the request
// ...
return next();
});
export default class MyController {
@get('/my-endpoint')
@myDecorator()
static async myHandler(req: VovkRequest) {
console.log(req.vovk.meta<{ hello: string }>()); // { hello: 'world' }
// ...
}
}The metadata is a keyβvalue object that is merged when you call vovk.meta multiple times with different keys.
// ...
req.vovk.meta({ foo: 'bar' });
req.vovk.meta({ baz: 'qux' });
console.log(req.vovk.meta<{ foo: string; baz: string }>()); // { foo: 'bar', baz: 'qux' }As you can see, a generic is required only when you want typeβsafe access to metadata. If you donβt provide a generic, the metadata is inferred from the argument.
To reset metadata, call req.vovk.meta(null), which clears the metadata object.
req.vovk.meta(null);
console.log(req.vovk.meta()); // {}To summarize:
- To set metadata, use
req.vovk.meta({ key: value }). It will return the metadata object typed as the passed object. - To access metadata, use
req.vovk.meta<{ key: Type }>(), which will return the metadata object typed as the passed generic type. - To reset metadata, use
req.vovk.meta(null), which will clear the metadata object.
Client-Side Meta with xMetaHeader Key
The TypeScript client can send custom metadata to the server in the x-meta header as a JSON string.
import { UserRPC } from 'vovk-client';
const user = await UserRPC.getUser({
params: { id: '123' },
meta: { hello: 'world' }, // pass metadata to the server via x-meta header
});The getUser controller method, as well as decorators, can access this metadata via req.vovk.meta under the xMetaHeader key:
import { get, type VovkRequest } from 'vovk';
export default class UserController {
@get('{id}')
static async getUser(req: VovkRequest) {
const meta = req.vovk.meta<{ xMetaHeader: { hello: string } }>();
console.log(meta.xMetaHeader); // { hello: 'world' }
// ...
}
}This design prevents serverβside metadata from being overwritten by client input, as client values are only exposed under the xMetaHeader key.