# Services (/docs/code/services)

---
title: Services
description: A service is a shared object you declare in one file. An SDK client, a cache, a piece of domain logic. QUESTPIE builds it, keeps it for as long as you asked, and hands it to every handler.
kind: guide
package: questpie
---

Where should the Stripe client live? Not in a module-level `new` that half your
files import. Put it in `services/` and read it off the handler context.

## Declare it

Put a file under `services/`. Import `service` from `questpie/services`. The
factory takes no name argument, because the filename is the key.

```ts title="src/questpie/server/services/blog.ts"
import { service } from "questpie/services";

const WORDS_PER_MINUTE = 200;

export default service({
	create: () => ({
		computeReadingTime(content: string): number {
			const words = content
				.replace(/<[^>]*>/g, " ")
				.trim()
				.split(/\s+/).length;
			return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));
		},
	}),
});
```

Then register it. `questpie add service blog` writes the file and runs this.

```bash
questpie generate   # registers blog, types it onto ctx.services.blog
```

## Use it

Hooks, routes, jobs, email templates and seeds all carry the same services.

```ts title="src/questpie/server/collections/posts.ts"
import { collection } from "#questpie/factories";

export const posts = collection("posts")
	.fields(({ f }) => ({
		title: f.text(255).required(),
		content: f.textarea(),
		readingTime: f.number(),
	}))
	.hooks({
		beforeChange: ({ data, services }) => {
			if (data.content) {
				data.readingTime = services.blog.computeReadingTime(data.content);
			}
		},
	});
```

`services.blog` is typed to whatever `create` returned. A misspelled name is a
compile error.

<Callout type="info" title="Services live on `ctx`, not on `app`">
	`app.collections` and `app.email` exist. `app.services` does not. Your
	services reach you through a handler context, or through `createContext()`
	from `#questpie` in a script. `app.createContext()` is a different call and
	carries no services.
</Callout>

## The filename is the key

`services/blog.ts` registers as `blog`. `services/capacity-planner.ts`
registers as `capacityPlanner`. Codegen turns kebab-case into camelCase and
uses the result as the key. Rename the file and you rename the service.

One file is one service. Codegen takes the default export, or the first named
export when there is no default. It skips `index.ts` and names starting with `_`.

## Dependencies come from `ctx`

There is no `deps` array. `create(ctx)` receives the app surface, so a
dependency is just something you read off `ctx`. The container builds that
dependency the moment you read it, part way through your own `create`.

```ts title="src/questpie/server/services/digest.ts"
import { service } from "questpie/services";

export default service({
	create: (ctx) => ({
		async sendWeekly(authorId: string, to: string) {
			const { docs } = await ctx.collections.posts.find({
				where: { authorId },
				limit: 10,
			});
			await ctx.email.sendTemplate({
				template: "weeklyDigest",
				to,
				input: { count: docs.length },
			});
		},
	}),
});
```

`ctx` carries `db`, `collections`, `globals`, `email`, `queue`, `storage`,
`kv`, `logger`, `search`, `realtime`, `t` and `app`. Your own services sit
where their `namespace` puts them. The default bucket is `ctx.services`.

## Two ways to write it

Both forms build the same state, and each chained method returns a new builder.

```ts
service({ lifecycle: "singleton", create: (ctx) => new Cache(ctx) });
service()
	.lifecycle("singleton")
	.create((ctx) => new Cache(ctx));
```

## Lifecycle

`lifecycle` decides how often `create` runs. Leave it out and you get
`"singleton"`.

| Value                   | When `create` runs                    | Use it for                                  |
| ----------------------- | ------------------------------------- | ------------------------------------------- |
| `"singleton"` (default) | Once, at app start                    | API clients, pools, caches, stateless logic |
| `"request"`             | Every time a handler context is built | Anything that reads the caller's session    |

<Callout type="warn" title="A singleton never sees the caller">
	Its `create` runs at startup, before any request exists. `ctx.session` is
	empty there. Take the session as a method argument instead, from the caller
	that has one.
</Callout>

## dispose

`dispose(instance)` runs on `app.destroy()`. Use it to close what you opened.
The tanstack-start starter wires that call to `SIGINT` and `SIGTERM`. On the
other starters, call `destroyApp()` from `#questpie` yourself.

```ts title="src/questpie/server/services/redis.ts"
import { service } from "questpie/services";
import { createClient } from "redis";

export default service({
	create: async () => {
		const client = createClient({ url: process.env.REDIS_URL });
		await client.connect();
		return client;
	},
	dispose: async (client) => {
		await client.quit();
	},
});
```

## namespace

`namespace` moves the instance somewhere else on `ctx`.

| Value                    | Lands at              | Example                    |
| ------------------------ | --------------------- | -------------------------- |
| omitted, or `"services"` | `ctx.services[key]`   | `ctx.services.billing`     |
| `null`                   | `ctx[key]`            | `ctx.billing`              |
| any other string         | `ctx[namespace][key]` | `ctx.integrations.billing` |

The chained `.namespace()` takes `string | null`, so it cannot say "default".
Omit the option instead. An empty string throws.

<Callout type="warn" title="`null` will not override a built-in">
	`app`, `db`, `session`, `services`, `queue`, `email`, `storage`, `kv`,
	`executor`, `logger`, `observability`, `search`, `realtime`, `collections`,
	`globals` and `t` land first. A `namespace: null` service with one of those
	keys is dropped without a warning. Pick another key.
</Callout>

## The built-ins are services too

`ctx.db`, `ctx.email`, `ctx.queue`, `ctx.storage`, `ctx.kv`, `ctx.logger`,
`ctx.search` and `ctx.realtime` are `service()` definitions in the core module.
Each is declared `namespace: null`, which is why they sit beside yours on one
context. You configure them rather than write them, so
[Infrastructure](/docs/infrastructure) covers what each one does.

<Callout type="info" title="Two unrelated `email` symbols">
	`questpie/services` exports an `email` that is the email-template factory,
	covered under [Emails](/docs/code/emails). `f.email()` is a field type. They
	share a name and nothing else.
</Callout>

## Where each topic lives

| Topic                                                      | Page                                         |
| ---------------------------------------------------------- | -------------------------------------------- |
| `"request"` in full, async `create`, circular dependencies | [Lifecycles](/docs/code/services/lifecycles) |
| Handlers that consume services                             | [Routes](/docs/code/routes)                  |
| Shipping services in a bundle someone else installs        | [Modules](/docs/code/modules)                |

## Next

**[Jobs](/docs/code/jobs)** is the same file convention for work that runs off
the request path, and its handler resolves services the same way.
