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.ts → ctx.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
| Lifecycle | Created | Destroyed | Use for |
|---|---|---|---|
"singleton" (default) | Once at app startup | App shutdown | External clients, SDKs, connection pools |
"request" | Per request | End of request | Tenant-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:
disposecalled at app shutdown - Request:
disposecalled 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
})