Skip to Content
🐺 Vovk.ts is released. Read the blog post →
Content Type

Content Type

The contentType option on a procedure controls how the request body is typed on the client, how it is parsed on the server, and which Content-Type headers are accepted. When contentType is set, the server returns a 415 Unsupported Media Type error if the incoming request doesn’t carry a matching Content-Type header.

The following table summarises how each content type family maps to client-side body typing and server-side req.vovk.body() return type:

Content TypeClient body typereq.vovk.body() return type
application/json, *+jsonTBody | BlobParsed JSON object
multipart/form-dataTBody | FormData | BlobParsed form object
application/x-www-form-urlencodedTBody | URLSearchParams | FormData | BlobParsed form object
text/*, text-like application types, *+xml, *+text, *+yaml, *+json-seqstring | Blobstring
Everything else (image/*, video/*, application/octet-stream, etc.)File | ArrayBuffer | Uint8Array | BlobFile
Note

Standard Request methods like req.json(), req.text(), req.blob(), and req.formData() remain available and work as usual—contentType only affects the typed helper on req.vovk.body().

Wildcard patterns such as 'video/*', 'image/*', or '*/*' are supported and match any subtype within that family.

JSON (default)

When no contentType is specified, or when it is set to 'application/json', the body is typed as the schema-inferred type and parsed as JSON.

import { z } from 'zod'; import { procedure, post, prefix } from 'vovk'; @prefix('users') export default class UserController { @post() static createUser = procedure({ body: z.object({ email: z.string().email(), name: z.string(), }), }).handle(async (req) => { const { email, name } = await req.vovk.body(); // { email: string; name: string } // ... }); }

Form Data

To accept form data requests, set contentType to 'multipart/form-data'.

import { z } from 'zod'; import { procedure, post, prefix } from 'vovk'; @prefix('users') export default class UserController { @post() static createUser = procedure({ contentType: 'multipart/form-data', body: z.object({ email: z.string().email(), name: z.string().min(2).max(100), }), }).handle(async (req) => { const { email, name } = await req.vovk.body(); // { email: string; name: string } // or use req.formData() for the raw FormData instance // ... }); }

The RPC method’s body type is inferred as TBody | FormData | Blob, where TBody is the type inferred from the validation schema (an object is converted to FormData on the client side, based on the content type).

import { UserRPC } from 'vovk-client'; const formData = new FormData(); formData.append('email', 'user@example.com'); formData.append('name', 'John Doe'); await UserRPC.createUser({ body: formData, });

On the server-side req.vovk.body() automatically parses the form data into a plain object (see req.vovk Interface). You can also access the raw FormData instance via req.formData().

If the form data can contain one or more values for the same key, use a union of the value type and an array of the value type, because FormData doesn’t distinguish between single and multiple values.

import { z } from 'zod'; import { procedure, post } from 'vovk'; export default class UserController { @post() static createUser = procedure({ contentType: 'multipart/form-data', body: z.object({ tags: z.union([z.array(z.string()), z.string()]), }), }).handle(async (req) => { const { tags } = await req.vovk.body(); // string | string[] // ... }); }

The same recommendation applies to files:

import { z } from 'zod'; import { procedure, post } from 'vovk'; export default class UserController { @post() static uploadFiles = procedure({ contentType: 'multipart/form-data', body: z.object({ files: z.union([z.array(z.file()), z.file()]), }), }).handle(async (req) => { const { files } = await req.vovk.body(); // File | File[] // ... }); }

Note that client-side validation does not fully support the OpenAPI-compatible format: "binary", and file size, type, etc., are not validated on the client side.

URL-Encoded Form Data

Set contentType to 'application/x-www-form-urlencoded' to accept URL-encoded form submissions. The body is parsed the same way as multipart/form-data, but the client body type also accepts URLSearchParams.

import { z } from 'zod'; import { procedure, post } from 'vovk'; export default class UserController { @post() static submitForm = procedure({ contentType: 'application/x-www-form-urlencoded', body: z.object({ username: z.string(), password: z.string(), }), }).handle(async (req) => { const { username, password } = await req.vovk.body(); // ... }); }

Text

For text-based content types (text/*, known text-like application types such as application/xml or application/yaml, and suffix patterns like *+xml, *+text, *+yaml, *+json-seq), the body is parsed as a string.

import { procedure, post } from 'vovk'; export default class UserController { @post() static importXml = procedure({ contentType: 'application/xml', }).handle(async (req) => { const xmlString = await req.vovk.body(); // string // ... }); }

Binary / File Uploads

For any content type that doesn’t fall into the above categories—such as application/octet-stream, image/*, video/*, application/pdf, etc.—the body is parsed into a File on the server. On the client side, the body accepts File | ArrayBuffer | Uint8Array | Blob.

import { procedure, post } from 'vovk'; export default class UserController { @post() static uploadImage = procedure({ contentType: 'image/*', }).handle(async (req) => { const file = await req.vovk.body(); // File // or use req.blob() for raw Blob access // ... }); }
import { UserRPC } from 'vovk-client'; const file = document.querySelector('input[type="file"]').files[0]; await UserRPC.uploadImage({ body: file, });

Multiple Content Types

contentType accepts an array of strings to allow multiple content types. The body type becomes a union of all corresponding types.

import { z } from 'zod'; import { procedure, post } from 'vovk'; export default class UserController { @post() static importData = procedure({ contentType: ['application/json', 'text/csv'], body: z.union([z.object({ items: z.array(z.string()) }), z.string()]), }).handle(async (req) => { const body = await req.vovk.body(); // parsed object or string, depending on the request // ... }); }
Last updated on