req.vovk Interface
The built-in NextRequest functions, such as req.json() and req.nextUrl.searchParams.get(), cover most cases. Vovk.ts also adds a vovk property to the request, with methods for more advanced input handling. Use them to:
- Get the data as the validation library transforms it (
req.json()andreq.nextUrl.searchParams.get()return it as sent), unless the procedure setspreferTransformedtofalse. - Parse nested query parameters.
- Read form data as a typed object, instead of the
FormDatathatreq.formData()returns. - Store request metadata.
- Write a handler that also runs in
.fn()calls and derived AI tools. Their request has onlyreq.vovk, soreq.json()andreq.nextUrlaren’t there.
async req.vovk.body()
req.vovk.body() returns the parsed request body. Mostly it works as req.json() does, but it also parses the non-JSON content types that procedure declares (see Content Type).
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 }
// ...
}
}Once a body schema or req.vovk.body() has read the body, the body methods of req, req.body and req.clone() replay it. To forward the request, use new Request(url, req), or pass body: req.body to fetch(). Don’t pass req or req.clone() as the first argument of new Request() or fetch(). In a route that doesn’t export dynamic = 'force-dynamic', Next.js gives the handler a Proxy of the request and of each clone, which they refuse. In any route, new Request(req) and fetch(req) take the request’s own body, which is read by then.
req.vovk.query()
req.vovk.query() returns the typed query parameters, nested data included.
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 goes in the query string with square brackets, known as “PHP-style” or “bracket notation”.
- Square brackets
[ ]hold the keys of arrays and nested objects. - Indices
[0]to[n-1], none missing, make an array of any length;[]appends an element after the highest index. - Any other keys make an object: named keys (such as
[f]and[u]), and numeric keys with gaps, sorecord[7]=ongives{ record: { 7: "on" } }. - A key given more than once collects its values into an array, so
tag=a&tag=bgives{ tag: ["a", "b"] }. A repeated index names the same element, so its last value wins. - With a
queryschema in a procedure, a key given once where the schema takes an array is a one-item array, sotag=agives{ tag: ["a"] }, as OpenAPI’s default style sends it. []followed by more brackets adds to the last element until that element already has the key:items[][name]=a&items[][price]=1&items[][name]=bgives{ items: [{ name: "a", price: "1" }, { name: "b" }] }.- Nesting goes up to 32 levels. A key with more brackets gets a 400 response.
- A
+is a space, as inURLSearchParams. Send a literal+as%2B, as the RPC clients do.
The RPC client leaves out null, undefined and empty objects or arrays, and numbers the remaining array items without gaps, so { tags: ['a', null, 'b'] } arrives as { tags: ['a', 'b'] }. A value with a toJSON method is sent as its result, as JSON.stringify does: a Date as an ISO string, a URL as its href.
This 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()
req.vovk.params() returns the typed route parameters, as the handler’s second argument does. To type it, use the third type argument of VovkRequest.
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 }
// ...
}
}A path segment can hold several params, such as @get('range/{from}-{to}') or @get('files/{name}.{ext}'). Each request gets its own params object. With a params schema, req.vovk.params() and the handler’s second argument both return the validated value.
req.vovk.meta()
req.vovk.meta() reads and writes custom metadata, 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 the request metadata
// ...
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. Calls with different keys merge into it, and every call returns it.
// ...
req.vovk.meta({ foo: 'bar' });
req.vovk.meta({ baz: 'qux' });
console.log(req.vovk.meta<{ foo: string; baz: string }>()); // { foo: 'bar', baz: 'qux' }A type argument is needed only to read the metadata with types. Without one, the type comes from the argument.
To clear the metadata, call req.vovk.meta(null):
req.vovk.meta(null);
console.log(req.vovk.meta()); // {}Client-Side Meta with xMetaHeader Key
The TypeScript client can send metadata to the server as a JSON string in the x-meta header.
import { UserRPC } from '@/client';
const user = await UserRPC.getUser({
params: { id: '123' },
meta: { hello: 'world' }, // sent in the x-meta header
});The getUser procedure and decorators read it with 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' }
// ...
}
}Client values stay under the xMetaHeader key, so client input can’t overwrite server-side metadata.