Service
A service module follows the ControllerβServiceβRepository pattern and separates business logic from request handlers. It keeps controllers focused on HTTP concerns, while the service encapsulates the business logic and data manipulation.
Like controllers, services are often written as static classes with static methods, but they do not require decorators or any special structure. The staticβclass style is simply a conventionβyou can instead use instantiated classes, standalone functions, or plain objects.
A key benefit of service methods is that their types can be inferred from the controller methods that call them, without triggering βimplicit anyβ TypeScript errors. Thanks to the legendary Anders HejlsbergΒ for the fix in #58616Β βwithout this TypeScript change, Vovk.ts would not 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 UserService.updateUser. That method, in turn, infers its types from the controller method, making the validation models (Zod schemas in this case) the single source of truth for input and output types, with no need to define separate types.
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 selfβreferencing type issues.