Skip to Content
ControllerService

Service class

Service is a part of the Controller-Service-Repository pattern that is used to separate business logic from the controller. It allows you to keep your controllers clean and focused on handling HTTP requests, while the service class contains the actual business logic and data manipulation.

Similarly to controllers, services are implemented as static classes with static methods that don’t require decorators or any special design. This means that the static class syntax for services is being just a convention and you can use any applicable way to define business logic: normal classes that need to be instantiated, separate functions, or plain objects.

The best thing about service methods is that their typing can be inferred from controller methods that call them, without causing β€œimplicit any” TS errors. Thanks to Anders Hejlsberg  for a solution for #58616 , without this fix to TypeScript language I wouldn’t continue working on Vovk.ts.

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(); return UserService.updateUser(body, query, params); }, }); }

As you can see, the handler method returns the result of the UserService.updateUser method. The method UserService.updateUser in its turn infers types from the controller method, making the validation models (at this case, Zod schemas) to be the only source of truth for the input and output types, without the need to define them as separate variables.

src/app/controller/service/page.mdx
import type { VovkBody, VovkOutput, VovkParams, VovkQuery } from 'vovk'; import type UserController from './UserController'; export default class UserService { static updateUser( body: VovkBody<typeof UserController.updateUser>, query: VovkQuery<typeof UserController.updateUser>, params: VovkParams<typeof UserController.updateUser> ) { // perform DB operations or other business logic here console.log(body, query, params); return { success: true, id: params.id } satisfies VovkOutput<typeof UserController.updateUser>; } }

In other words, service methods can infer types from the controller methods and controller methods can call service methods without causing self-referencing issues.

Last updated on