07 — Service Layer
Goal: Add behaviour that does not belong to one model: a configurable Publisher service and an auto-instantiated @Bean, both exposing methods with @Operation.
Files touched: src/services/publisher.service.ts, src/services/bean.service.ts, webda.config.json.
Concepts: Service.Parameters, @WebdaModda, @Bean, @Operation on service methods, custom REST routes, useLog, service lifecycle.
Walkthrough
1. Create src/services/publisher.service.ts
import { Operation, Service, useLog } from "@webda/core";
export class PublisherParameters extends Service.Parameters {}
/**
* @WebdaModda
*/
export class Publisher<T extends PublisherParameters = PublisherParameters> extends Service<T> {
static Parameters = PublisherParameters;
@Operation()
publish(message: string): string {
useLog("INFO", "Publishing message:", message);
return "customid";
}
@Operation()
async publishPost(postId: string): Promise<{ postId: string; status: string }> {
useLog("INFO", "Publishing post with ID:", postId);
return { postId, status: "published" };
}
}
- File name — services live in
*.service.tsfiles; the build fails on a service file without that suffix. @WebdaModda— this JSDoc tag registers the class inwebda.module.jsonasMyBlog/Publisher, sowebda.config.jsoncan instantiate it.Service.Parameters— the typed parameters of the service. Each field becomes part of the configuration schema (.webda/config.schema.json) and is read asthis.parameters.<field>. Use JSDoc tags for validation and a default value for optional fields, e.g.delayHours: number = 24;.@Operation()— the operation id is the configured service name, capitalized, then the method name:Publisher.Publish,Publisher.PublishPost. REST exposes it asPUT /publisher/publishwith the parameters read from the body; GraphQL and gRPC transports read the same operations.useLog— log through Webda's logger, neverconsole.log.
2. Enable it in webda.config.json
{
"services": {
"Publisher": {
"type": "Publisher"
}
}
}
"type": "Publisher" resolves to MyBlog/Publisher: types of your own application can omit the namespace. Services are always created by the framework from this configuration; never new Publisher(...) them yourself. Another service reaches it with useService("Publisher").
3. A @Bean with a custom route
A @Bean is a service of the application that is instantiated automatically, without a configuration entry:
import { Bean, Operation, RestParameters, Service, useApplication, useLog } from "@webda/core";
@Bean
export class TestBean extends Service {
/**
* Get the version of the application
* @returns version of the application
*/
@Operation<RestParameters>({
id: "Version.Get",
rest: { method: "get", path: "/version" },
description: "Get the version of the application"
})
async version(): Promise<string> {
return useApplication().getPackageDescription().name;
}
@Operation()
async testOperation(counter: number): Promise<string> {
useLog("INFO", `Test operation called with counter: ${counter}`);
return counter.toString(16);
}
}
@Operation options override the defaults: id renames the operation (Version.Get instead of TestBean.Version), and rest sets the HTTP method and path. Without options, testOperation is exposed as PUT /testbean/testoperation.
The sample's TestBean also memoizes getVersion() with @InstanceCache and has a demonstrateTypeSafety operation; see sample-apps/blog-system/src/services/bean.service.ts.
4. Lifecycle hooks (when you need them)
A service connecting to an external system uses the lifecycle hooks:
resolve(): this {
super.resolve();
// Synchronous: read and check the configuration, resolve dependencies
return this;
}
async init(): Promise<this> {
await super.init();
// Asynchronous: open connections, start timers
return this;
}
async stop(): Promise<void> {
// Release what init created
await super.stop();
}
The framework calls them in order: constructor → resolve() → init() → running → stop(). Always call super.
5. Rebuild and restart
npm run debug # or npm run serve
Verify
curl -s -X PUT http://localhost:18080/publisher/publish \
-H "Content-Type: application/json" -d '{"message":"Hello from REST"}'
# → customid
curl -s -X PUT http://localhost:18080/publisher/publishpost \
-H "Content-Type: application/json" -d '{"postId":"hello-world"}'
# → {"postId":"hello-world","status":"published"}
curl -s http://localhost:18080/version
# → my-blog (the "name" of package.json)
curl -s -X PUT http://localhost:18080/testbean/testoperation \
-H "Content-Type: application/json" -d '{"counter":42}'
# → 2a
Service operations have no model, so no canAct applies: they are open to every caller. Check useContext().getCurrentUserId() inside the method when an operation must be restricted.