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:
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.
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.