A production-ready TypeScript boilerplate for building distributed task workers using the MapColonies Jobnik SDK.
- Type-safe task handling with full TypeScript support for job and stage definitions
- Built-in observability with distributed tracing, Prometheus metrics, and structured logging
- Production-ready containerization with multi-stage Docker builds and Helm charts
- Dependency injection using tsyringe for clean, testable architecture
- Health checks and graceful shutdown with @godaddy/terminus
- Comprehensive testing setup with Vitest for unit and integration tests
- Node.js >= 24.0.0
- Connection to a Jobnik Job Manager API instance
npm installConfigure the worker by editing files in the config/ directory:
default.json- Base configurationdevelopment.json- Development overridesproduction.json- Production overridestest.json- Test environment settingslocal.json- Local overrides (not committed to version control)
Set the Jobnik Job Manager API URL via Helm values or environment variables.
npm run start:devDevelopment mode enables offline config mode and source map support for better debugging.
npm run build
npm start# Run all tests with coverage
npm test
# Watch mode for development
npm run test:watchnpm run test:integration needs an S3-compatible server. By default it starts a Minio
testcontainer automatically.
The image is pinned to the release deployed in our Azure environment; bump it in
tests/integration/helpers/minioContainer.ts when that environment moves.
To run against an already-running Minio instead, set TEST_MINIO_ENDPOINT:
| Variable | Default | Purpose |
|---|---|---|
TEST_MINIO_ENDPOINT |
(unset) | Point the suite at an existing Minio. Unset means start a container. |
TEST_MINIO_ACCESS_KEY |
minioadmin |
Access key for that server. |
TEST_MINIO_SECRET_KEY |
minioadmin |
Secret key for that server. |
The suite creates and deletes buckets on whichever endpoint it is given. Never point
TEST_MINIO_ENDPOINTat a shared or deployed environment, and beware of leaving it exported in a shell profile. Each run prints which mode it selected and against which endpoint.
This boilerplate includes example "logistics" code to demonstrate task handling. Follow these steps to adapt it to your use case:
Delete the example logistics implementation:
rm -rf src/logistics src/seeder.ts tests/logistics.spec.tsRemove the seeder call from src/index.ts:
// REMOVE THESE LINES:
const sdk = container.resolve<LogisticsSDK>(SERVICES.JOBNIK_SDK);
await seedData(sdk.getProducer());Create a new types file (e.g., src/yourDomain/types.ts):
import type { IJobnikSDK } from '@map-colonies/jobnik-sdk';
export interface YourJobTypes {
jobType1: {
data: { /* your job data schema */ };
userMetadata: { /* your job metadata */ };
};
}
export interface YourStageTypes {
stage1: {
data: { /* stage data schema */ };
userMetadata: { /* stage metadata */ };
task: {
data: { /* task data schema */ };
userMetadata: { /* task metadata */ }
};
};
}
export type YourSDK = IJobnikSDK<YourJobTypes, YourStageTypes>;Create your manager (e.g., src/yourDomain/manager.ts):
import { injectable } from 'tsyringe';
import type { Task, TaskHandlerContext } from '@map-colonies/jobnik-sdk';
@injectable()
export class YourManager {
public async handleYourTask(
task: Task<YourStageTypes['stage1']['task']>,
context: TaskHandlerContext<YourJobTypes, YourStageTypes, 'jobType1', 'stage1'>
): Promise<void> {
context.logger.info({ msg: 'Processing task', taskId: task.id });
// Your task processing logic here
await context.updateStageUserMetadata({ /* updated metadata */ });
}
}Modify src/worker.ts to use your new types and handler:
import { YourManager } from './yourDomain/manager';
import type { YourSDK } from './yourDomain/types';
export const workerBuilder: FactoryFunction<IWorker> = (container: DependencyContainer) => {
const sdk = container.resolve<YourSDK>(SERVICES.JOBNIK_SDK);
const manager = container.resolve(YourManager);
const worker = sdk.createWorker<'jobType1', 'stage1'>(
'stage1',
manager.handleYourTask.bind(manager),
config.get('jobnik.worker')
);
return worker;
};Update references to cleaner in:
package.json- name, description, authorhelm/Chart.yaml- name, descriptionhelm/values.yaml- mclabels, configManagement.name, image.repositoryhelm/templates/_helpers.tpl- all template definitionshelm/templates/*.yaml- review all template files for hardcoded references
Edit package.json:
{
"name": "your-worker-name",
"description": "Your worker description",
"author": "Your Team"
}Prometheus metrics are exposed on the /metrics endpoint (default port 8080). Key metrics include:
- Worker task processing duration
- Task success/failure rates
- Active task count
- Custom application metrics
Distributed tracing can be enabled in config/default.json:
{
"telemetry": {
"tracing": {
"isEnabled": true,
"url": "http://your-otlp-collector:4318/v1/trace"
}
}
}Structured JSON logging is provided by @map-colonies/js-logger. Configure log level:
{
"telemetry": {
"logger": {
"level": "info",
"prettyPrint": false
}
}
}Deploy using Helm:
helm install your-worker-name ./helm