Service
A service module is part of the Controller-Service-Repository pattern, used to separate business logic from request handlers. 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.
Similar to controllers, services are implemented as static classes with static methods, but they don’t require the use of decorators or any special design. This means that the static class syntax for services is merely 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 feature of service methods is that their typing can be inferred from controller methods that call them, without causing “implicit any” TypeScript errors. Thanks to the legendary Anders Hejlsberg for the solution to #58616 —without this fix to the TypeScript language, Vovk.ts wouldn’t be possible.
Let’s say you have the following controller class:
import { z } from 'zod';
import { prefix, post, operation } from 'vovk';
import { withZod } from 'vovk-zod';
import UserService from './UserService';
@prefix('users')
export default class UserController {
@operation({
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 handle
method returns the result of the UserService.updateUser
method. The UserService.updateUser
method, in turn, infers types from the controller method, making the validation models (in this case, Zod schemas) the only source of truth for the input and output types, without the need to define them as separate variables.
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 controller methods, and controller methods can call service methods without causing self-referencing issues.