Skip to content

Service builder

The service builder is a powerful tool to define a service. The builder helps to keep the code unified, clear, and understandable and takes care of configs, types, and so on.

To create a new service, please use the CLI command.

bash
purista add service

This will generate all necessary files and structures for you. In most cases, you only might want to add/change the service config or define resources for commands and subscriptions.

Adding commands and subscriptions can also be done via CLI, and does not require manual changes in the service files.

File structure

Service definition

As the first step, we will need to set up a service. This can be done by using the service builder.

typescript
import { ServiceBuilder, ServiceInfoType } from '@purista/core'

export const myServiceInfo = {
  serviceName: 'MyService',
  serviceVersion: '1',
  serviceDescription: 'my service',
} as const satisfies ServiceInfoType

export const myServiceV1ServiceBuilder = new ServiceBuilder(myServiceInfo)

💪 Use strong types

Use as const satisfies ServiceInfoType = {...} instead of export const myServiceInfo: ServiceInfoType = {...} for better types.

⚠️ Avoid cycling dependencies

The basic service definition (name, version, config) and adding the commands and subscriptions should be done in separate files, to prevent cycling dependencies. The command and subscription builders are generated from the basic service builder, to propagate config & custom classes correctly.

Add commands

Adding commands to a service builder can be done by using the addCommandDefinition method. It takes one or more command definitions as parameters, which are generated by the command builder.

typescript
import { pingCommandBuilder } from './command/ping/index.js'
import { fooCommandBuilder } from './command/foo/index.js'
import { myServiceV1ServiceBuilder } from './myServiceV1ServiceBuilder'

const commandDefinitions: Parameters<typeof myServiceV1ServiceBuilder['addCommandDefinition']>[0][] = [
  pingCommandBuilder.getDefinition(),
  fooCommandBuilder.getDefinition()
]

export const myServiceV1Service = myServiceV1ServiceBuilder
  .addCommandDefinition(...commandDefinitions)
typescript
import { ServiceBuilder, ServiceInfoType } from '@purista/core'

export const myServiceInfo = {
  serviceName: 'MyService',
  serviceVersion: '1',
  serviceDescription: 'my service',
} as const satisfies ServiceInfoType

export const myServiceV1ServiceBuilder = new ServiceBuilder(myServiceInfo)

TIP

The additional const commandDefinitions is used by the CLI tool. In regular use cases, you usually do not need to modify this file manually.

Add subscriptions

typescript
import { pingCommandBuilder } from './command/ping/index.js'
import { fooCommandBuilder } from './command/foo/index.js'
import { barSubscriptionBuilder } from './subscription/bar/index.js'
import { myServiceV1ServiceBuilder } from './myServiceV1ServiceBuilder'

const commandDefinitions: Parameters<typeof myServiceV1ServiceBuilder['addCommandDefinition']>[0][] = [
  pingCommandBuilder.getDefinition(),
  fooCommandBuilder.getDefinition()
]

const subscriptionDefinitions: Parameters<typeof myServiceV1ServiceBuilder['addSubscriptionDefinition']>[0][] = [  
  barSubscriptionBuilder.getDefinition()  
]  

export const myServiceV1Service = myServiceV1ServiceBuilder
  .addCommandDefinition(...commandDefinitions)
  .addSubscriptionDefinition(...subscriptionDefinitions)  
typescript
import { ServiceBuilder, ServiceInfoType } from '@purista/core'

export const myServiceInfo = {
  serviceName: 'MyService',
  serviceVersion: '1',
  serviceDescription: 'my service',
} as const satisfies ServiceInfoType

export const myServiceV1ServiceBuilder = new ServiceBuilder(myServiceInfo)

TIP

The additional const commandDefinitions and const subscriptionDefinitions constants are used by the CLI tool. In regular use cases, you usually do not need to modify this file manually.

Keep Generated Definition Lists Typed

Do not remove the type aliases and typed declaration lists generated by the CLI.

  • Keep the constant names commandDefinitions and subscriptionDefinitions.
  • Keep the declaration lists typed via the builder: const commandDefinitions: Parameters<typeof myServiceV1ServiceBuilder['addCommandDefinition']>[0][] = [] and const subscriptionDefinitions: Parameters<typeof myServiceV1ServiceBuilder['addSubscriptionDefinition']>[0][] = []
  • Do not replace these declarations with broad or untyped variants like any[].

These declarations are important for CLI update operations and for preserving end-to-end type inference.

Create a service instance

The service builder is able to create a new instance of a service. The most minimal version is, to call the getInstance method of the service builder and to provide a event bridge instance as first parameter.

typescript
const myService = await myServiceV1Service.getInstance(eventBridge)

At this point, we've created a service instance, but the instance is not fully connected to the event bridge and has not announced the existence of the commands and subscriptions.

We need to start the service, to get a fully working service instance:

typescript
const myService = await myServiceV1Service.getInstance(eventBridge)
await myService.start() 

There are two reasons for this 2-step-behaviour.

  1. The regular TypeScript constructor cannot be an async function, which makes it hard to handle async method calls, like sending info messages to the event bridge.
  2. If you are using a custom service class, you might need to do some logic on startup, and only on success, the service should fully connect the event bridge.

Instance options

The getInstance provides a second, optional parameter.