Skip to content

Dependency Injection

Pass external services to your handlers through the router's dependency system.

Providing Dependencies

Pass a deps object as the second argument to WorkflowRouter:

ts
type TaskDeps = {
	db: Database;
	emailService: EmailService;
};

const taskDeps: TaskDeps = {
	db: new Database(),
	emailService: new EmailService(),
};

const taskRouter = new WorkflowRouter(taskWorkflow, taskDeps);

The type is inferred from the object you pass. All handlers and middleware receive the same typed deps.

Accessing Dependencies

Use deps in any handler or middleware:

ts
accessRouter.state("Review", ({ on }) => {
	on("Approve", async ({ deps, data, error, transition }) => {
		const canApprove = deps.reviewService.canApprove(data.reviewerId);
		if (!canApprove) {
			error({ code: "NotReviewer", data: { expected: data.reviewerId } });
		}

		transition("Published", {
			title: data.title,
			body: data.body,
			publishedAt: new Date(),
		});
	});
});

deps is fully typed -- TypeScript knows exactly what services are available.

Complete Example

ts
const articleWorkflow = defineWorkflow("article", {
	states: {
		Draft: z.object({ title: z.string(), body: z.string().optional() }),
		Published: z.object({ title: z.string(), body: z.string(), publishedAt: z.coerce.date() }),
	},
	commands: {
		Publish: z.object({}),
	},
	events: {
		ArticlePublished: z.object({ articleId: z.string(), notifiedSubscribers: z.number() }),
	},
	errors: {
		BodyRequired: z.object({}),
	},
});

// Define dependencies
type _Deps = {
	notifier: { notifySubscribers(articleId: string): Promise<number> };
};

const router = new WorkflowRouter(articleWorkflow, {
	notifier: {
		async notifySubscribers(_articleId: string) {
			// send emails, push notifications, etc.
			return 42;
		},
	},
});

// Use deps in handler
router.state("Draft", ({ on }) => {
	on("Publish", async ({ data, deps, error, transition, emit, workflow }) => {
		if (!data.body) {
			error({ code: "BodyRequired", data: {} });
		}

		const count = await deps.notifier.notifySubscribers(workflow.id);

		transition("Published", {
			title: data.title,
			// biome-ignore lint/style/noNonNullAssertion: guarded by error() check above
			body: data.body!,
			publishedAt: new Date(),
		});

		emit({
			type: "ArticlePublished",
			data: { articleId: workflow.id, notifiedSubscribers: count },
		});
	});
});

Testing with Mock Dependencies

Dependency injection makes testing straightforward -- pass mocks instead of real services:

ts
const mockRouter = new WorkflowRouter(articleWorkflow, {
	notifier: {
		async notifySubscribers() {
			return 0; // no-op in tests
		},
	},
});