Skip to main content

Service Runner

ServiceRunner is a convenience wrapper around CreateApplication for running a single service function. It's designed for scripts, CLI tools, migration runners, and other short-lived processes where you want access to DA's DI, lifecycle, and config system without the ceremony of declaring a full application module.

Basic usage​

import { ServiceRunner } from "@digital-alchemy/core";

await ServiceRunner(
{
libraries: [MY_LIB],
configuration: {
my_script: {
TARGET_ID: { type: "string", required: true },
},
},
},
async ({ logger, config, my_lib }) => {
const id = config.my_script.TARGET_ID;
logger.info({ id }, "starting script");
await my_lib.doWork(id);
logger.info("done");
},
);

The second argument is a service function that receives TServiceParams, including any libraries you declare. The application bootstraps, runs your function, and then tears down.

Signature​

async function ServiceRunner<C, NAME extends string = "dynamic">(
options: ServiceRunnerConfiguration<C, NAME> & { bootstrap?: BootstrapOptions },
service: (params: LocalServiceParams<C, NAME>) => void | Promise<void>,
): Promise<void>

ServiceRunnerConfiguration accepts the same options as CreateApplication except name, services, and priorityInit (which are managed internally). You can optionally set a name for the module (defaults to "dynamic"), which determines the config namespace:

await ServiceRunner(
{
name: "migrate", // config lives at config.migrate.*
configuration: {
migrate: {
DB_URL: { type: "string", required: true },
},
},
bootstrap: {
configSources: { file: false },
},
},
async ({ config, logger }) => {
logger.info({ url: config.migrate.DB_URL }, "running migrations");
// ...
},
);

When to use ServiceRunner vs CreateApplication​

ScenarioUse
Full application with multiple services, long-running processCreateApplication
One-off script, migration, seed, health checkServiceRunner
CLI tool that does a single operation and exitsServiceRunner
Application with complex inter-service wiringCreateApplication
Quick utility that needs libraries and configServiceRunner

ServiceRunner internally calls CreateApplication with a single service named "service". It has the same lifecycle (PreInit → PostConfig → Bootstrap → Ready → shutdown), the same config system, and the same library wiring. The only difference is that you don't declare a module definition file — you just pass the function.

Libraries and config​

ServiceRunner supports libraries, configuration, and a nested bootstrap object for BootstrapOptions:

await ServiceRunner(
{
libraries: [DATABASE_LIB, HTTP_LIB],
configuration: {
my_script: {
DRY_RUN: { type: "boolean", default: false },
BATCH_SIZE: { type: "number", default: 100 },
},
},
bootstrap: {
loggerOptions: { pretty: true },
configuration: {
my_script: { DRY_RUN: true }, // override for this run
},
},
},
async ({ config, database, http, logger }) => {
if (config.my_script.DRY_RUN) {
logger.warn("dry run — no writes");
}
// use database and http normally
},
);

Lifecycle in scripts​

Because ServiceRunner boots a full DA application, lifecycle hooks work normally. If a library registers onBootstrap callbacks (database connections, HTTP clients), those run before your service function executes. Teardown also runs automatically after your function returns.

If you need to do work at a specific lifecycle stage, use lifecycle from params:

await ServiceRunner({}, async ({ lifecycle, logger }) => {
lifecycle.onPreShutdown(() => {
logger.info("cleaning up before shutdown");
});

// main work here — runs at Ready
await doWork();
});
note

ServiceRunner always runs the full lifecycle including teardown. If your script throws, teardown still runs (same as a production application receiving SIGTERM).