QUESTPIE
Build Your BackendBusiness Logic

Services

Singleton and request-scoped services — define once, inject everywhere via AppContext.

Services are reusable units of logic that get injected into AppContext under the services key. Define a service in services/, and it becomes available in every hook, route, and job handler.

Defining a Service

services/blog.ts
import { service } from "questpie";

const WORDS_PER_MINUTE = 200;

function stripHtml(html: string): string {
	return html.replace(/<[^>]*>/g, " ");
}

export default service({
	lifecycle: "singleton",
	create: () => ({
		computeReadingTime(content: string): number {
			const text = stripHtml(content);
			const words = text
				.trim()
				.split(/\s+/)
				.filter((w) => w.length > 0).length;
			return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));
		},

		generateSlug(title: string): string {
			return title
				.toLowerCase()
				.replace(/[^a-z0-9\s-]/g, "")
				.trim()
				.replace(/\s+/g, "-");
		},
	}),
});

The filename becomes the key inside services: services/blog.tsctx.services.blog.

Using Services

Services are available in every handler via services destructuring:

collections/posts.ts
export const posts = collection("posts")
	.fields(({ f }) => ({
		title: f.text().required().label("Title"),
		content: f.textarea().label("Content"),
		readingTime: f.number().label("Reading Time"),
		slug: f.text().label("Slug"),
	}))
	.hooks({
		beforeChange: async ({ data, services }) => {
			const { blog } = services;
			if (data.content) {
				data.readingTime = blog.computeReadingTime(data.content);
			}
			if (data.title) {
				data.slug = blog.generateSlug(data.title);
			}
		},
	});
routes/get-post-stats.ts
export default route()
	.post()
	.schema(z.object({ postId: z.string() }))
	.handler(async ({ input, collections, services }) => {
		const post = await collections.posts.findOne({
			where: { id: input.postId },
		});
		return {
			readingTime: services.blog.computeReadingTime(post.content),
			excerpt: services.blog.extractExcerpt(post.content),
		};
	});

Lifecycle

LifecycleCreatedDestroyedUse for
"singleton" (default)Once at app startupApp shutdownExternal clients, SDKs, connection pools
"request"Per requestEnd of requestTenant-scoped DB, user-specific config

Singleton (default)

services/stripe.ts
import { service } from "questpie";
import Stripe from "stripe";

export default service({
	lifecycle: "singleton",
	create: () => new Stripe(process.env.STRIPE_SECRET_KEY!),
});

Request-scoped

services/tenant-db.ts
import { service } from "questpie";

export default service({
	lifecycle: "request",
	deps: ["db", "session"] as const,
	create: ({ db, session }) => {
		return createScopedDb(db, session?.user?.tenantId);
	},
	dispose: (scopedDb) => scopedDb.release(),
});

Dependencies

Services can depend on other services and infrastructure via deps. Use as const for type safety:

services/analytics.ts
import { service } from "questpie";

export default service({
	deps: ["db", "logger"] as const,
	create: ({ db, logger }) => {
		logger.info("Analytics service initialized");
		return new AnalyticsService(db);
	},
});

Available dependencies include: db, logger, kv, email, queue, storage, search, realtime, session, and any other user-defined services.

Disposal

Optional dispose function for cleanup:

export default service({
	lifecycle: "singleton",
	create: () => {
		const pool = createConnectionPool();
		return pool;
	},
	dispose: async (pool) => {
		await pool.close();
	},
});
  • Singleton: dispose called at app shutdown
  • Request: dispose called at end of each request

API Reference

service({
  lifecycle?: "singleton" | "request",  // default: "singleton"
  deps?: readonly string[],             // dependencies from AppContext
  create: (deps) => TInstance,          // factory function
  dispose?: (instance) => void | Promise<void>,  // cleanup
})
  • Routes — Use services in route handlers
  • Hooks — Use services in lifecycle hooks
  • Codegen — How services appear in AppContext

On this page