Skip to Content
ControllerGetting started

Controller

Create a controller with CLI

npx vovk new controller admin/user # Create a new controller for the "admin" segment

vovk new documentation

Definition

Controller is a static class (a class that has static methods only and it’s never instantiated) that handles incoming HTTP requests.

Methods of this class, that are decorated with an HTTP decorator (like @get() or @post()), become request handlers and accept 2 arguments: VovkRequest (that is inherited from NextRequest) and parameters that are defined by the decorator path.

The NextRequest type doesn’t provide any information about the request body or query parameters, so the body is casted as any and query parameters are casted as string | null.

import type { NextRequest } from 'next'; import { prefix, put } from 'vovk'; @prefix('users') export default class UserController { // Example request: PUT /api/users/69?role=moderator @put('{id}') static async updateUser(req: NextRequest, { id }: { id: string }) { const data = await req.json(); // any const userRole = req.nextUrl.searchParams.get('role'); // string | null // ... return updatedUser; } }

To add the required body and query types you need replace NextRequest by VovkRequest and define the types as generic arguments.

src/modules/user/UserController.ts
import { prefix, put, type VovkRequest } from 'vovk'; import type { User } from '../../types'; @prefix('users') export default class UserController { // Example request: PUT /api/users/69?role=moderator @put('{id}') static async updateUser( req: VovkRequest<Partial<User>, { notify: 'email' | 'push' | 'none' }>, { id }: { id: string } ) { const data = await req.json(); // Partial<User> const notify = req.nextUrl.searchParams.get('notify'); // 'email' | 'push' | 'none' // ... return updatedUser; } }
ℹ️

Hint: to make sure that the class is never instantiated, you can make it abstract.

export default abstract class UserController { /* ... */ }

As you can see we’ve changed nothing more than the type of req but now data receives type of Partial<User> and userRole is casted as 'user' | 'moderator' | 'admin' and does not extend null anymore.

Validation

To validate the request body, query parameters, and headers you can use validation libraries. At this case you don’t need to use VovkRequest manually, as the validation library will define the request type by itself.

src/modules/user/UserController.ts
import { z } from 'zod'; import { withZod } from 'vovk-zod'; import { prefix, post, openapi } from 'vovk'; @prefix('users') export default class UserController { @openapi({ summary: 'Update user (Zod)', description: 'Update user by ID with Zod validation', }) @post('{id}') static updateUser = withZod({ body: z .object({ name: z.string().meta({ description: 'User full name' }), age: z.number().min(0).max(120).meta({ description: 'User age' }), email: z.email().meta({ description: 'User email' }), }) .meta({ description: 'User object' }), params: z.object({ id: z.uuid().meta({ description: 'User ID' }), }), query: z.object({ notify: z.enum(['email', 'push', 'none']).meta({ description: 'Notification type' }), }), output: z .object({ success: z.boolean().meta({ description: 'Success status' }), }) .meta({ description: 'Response object' }), async handle(req, { id }) { const { name, age } = await req.json(); const notify = req.nextUrl.searchParams.get('notify'); // do something with the data console.log(`Updating user ${id}:`, { name, age, notify }); return { success: true, }; }, }); }

Initialization

Once the controller is defined it needs to be initialized at the wildcard route (also called segment) by adding it to the controllers object. The key of this object defines the name of the resulting library variable exported from "vovk-client" or segmented client module.

src/app/api/[[...vovk]]/route.ts
import { initSegment } from 'vovk'; import UserController from '../../../modules/user/UserController'; const controllers = { UserRPC: UserController, }; export type Controllers = typeof controllers; export const { GET, POST, PUT, DELETE } = initSegment({ controllers });

This will β€œcompile” the UserController into UserRPC that performs fetch requests to the /api/users subpath.

import { UserRPC } from 'vovk-client'; // performs PUT /api/users/69?notify=push const updatedUser = await UserRPC.updateUser({ query: { notify: 'push' }, params: { id: '69' }, body: { ...userData }, });

Auto-generated endpoints

All HTTP decorators provide .auto method that generates endpoint name automatically from the method name that makes the handler definition more RPC-ish.

src/modules/user/UserController.ts
import { prefix, put } from 'vovk'; @prefix('users') export default class UserController { // creates PUT /api/users/do-something @put.auto() static async doSomething(/* ... */) { // ... } }

Return types

Custom object

The HTTP-decorated static methods of controllers can return several kinds of objects. For example, a regular object literal.

// ... export default class HelloController { @get() static helloWorld() { return { hello: 'world' }; } }

Another example - if the controller method returns Prisma ORM invocation the type is going to be recognised accordingly.

// ... export default class UserController { @get() static async updateUser(/* ... */) { // ... return prisma.user.update({ where: { id }, data, }); } }

At this case the returned value of client method UserRPC.updateUser gets User type generated at @prisma/client.

Response object

HTTP handlers can also return regular Response object, including NextResponse.

// ... export default class HelloController { @get() static helloWorld() { return new Response(JSON.stringify({ hello: 'world' }), { headers: { 'Content-Type': 'application/json' }, }) as unknown as { hello: string }; } }

When NextResponse is returned from the controller method, the client library is going to recognise the return type as expected.

import { NextResponse } from 'next/server'; // ... export default class HelloController { @get() static helloWorld() { return NextResponse.json({ hello: 'world' }, { status: 200 }); } }
Last updated on