# Getting started (/docs/composer/getting-started)

> For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL.

Build a two-service Prisma App from an empty directory and run it on your machine with one command.

Location: Composer > Getting started

By the end of this page you will have a working Prisma App: a `quotes` API and a public `gateway` that calls it. A typed contract wires the two together, and one command runs them on your machine. Along the way you will meet every core Composer idea once: a contract, a service, a root module, a build, and a local run.

The app is deliberately tiny, with no database, so you can see the whole shape at once. Adding a Postgres is the natural next step. See [Databases](https://www.prisma.io/docs/composer/databases) when you are ready.

> [!NOTE]
> Working with a coding agent
> 
> Add the Composer skill first, even if you plan to write every line yourself. It ships inside the `@prisma/composer` package, and one command, run after step 1 installs the packages, copies it into the directories your agent reads and stops the agent from inventing an API that does not exist:
> 
> 
>   
> 
>   #### bun

>     ```bash
>     bunx --bun prisma@latest init
>     ```
>
> 
>   #### pnpm

>     ```bash
>     pnpm dlx prisma@latest init
>     ```
>
> 
>   #### yarn

>     ```bash
>     yarn dlx prisma@latest init
>     ```
>
> 
>   #### npm

>     ```bash
>     npx prisma@latest init
>     ```
>
> 
> 
> `init` also adds a `postinstall` hook that keeps the skill matching the installed version; see [`skills`](https://www.prisma.io/docs/cli/skills).

## Prerequisites [#prerequisites]

* **Node.js 22.18 or newer.** Check with `node --version`. Composer hands your TypeScript entry file straight to Node. Node runs `.ts` files directly only from 22.18.0, the release that turns type stripping on by default. On any older version, including 22.17, Composer commands stop at `ERR_UNKNOWN_FILE_EXTENSION` naming your own entry file.
* **[Bun](https://bun.sh).** Prisma Compute runs Bun, so the server code targets it (`Bun.serve`). This guide also uses `bun build` to produce the built output.
* **npm, pnpm, or Bun.** Bun workspaces need two extra direct dependencies; see the note in step 1.

You do not need a Prisma account for this page. Local runs never talk to the platform. You only need credentials when you [deploy](https://www.prisma.io/docs/composer/deploying).

## 1. Set up the project [#1-set-up-the-project]

Create a directory and install the two Composer packages, plus [arktype](https://arktype.io) for the contract schemas:

  

#### bun

```bash
bun init
npm pkg set type module
# couldn't auto-convert command
bun add @prisma/composer @prisma/composer-prisma-cloud arktype
bun add --dev typescript @types/bun
```

#### pnpm

```bash
pnpm init
npm pkg set type=module
# couldn't auto-convert command
pnpm add @prisma/composer @prisma/composer-prisma-cloud arktype
pnpm add -D typescript @types/bun
```

#### yarn

```bash
yarn init
npm pkg set type=module
# couldn't auto-convert command
yarn add @prisma/composer @prisma/composer-prisma-cloud arktype
yarn add --dev typescript @types/bun
```

#### npm

```bash
npm init
npm pkg set type=module
npm install @prisma/composer @prisma/composer-prisma-cloud arktype
npm install -D typescript @types/bun
```

`"type": "module"` matters: Composer loads your entry file as an ES module, and without it every Composer command stops at `COMPOSE.ENTRY_UNLOADABLE` ("Cannot use import statement outside a module").

Then pin `effect` in your `package.json` to the exact version your installed `@prisma/composer` requires. This works around an upstream bug in Composer's deploy engine, whose own version ranges float past the versions its code supports. Without the pin, a fresh install ends up with two conflicting copies of `effect`, and every Composer command refuses to run with `DEPS.EFFECT_VERSION_CONFLICT`, naming the version to pin.

Read the required version from `dependencies.effect` in `node_modules/@prisma/composer/package.json` (for `@prisma/composer@0.13.0` it is `4.0.0-rc.111`), and pin it:

```json title="package.json (npm)"
{
  "overrides": {
    "effect": "4.0.0-rc.111"
  }
}
```

With pnpm, the same map goes under `"pnpm"` instead:

```json title="package.json (pnpm)"
{
  "pnpm": {
    "overrides": {
      "effect": "4.0.0-rc.111"
    }
  }
}
```

Run your install again after adding the block. (Yarn users: the equivalent field is `"resolutions"`. Bun reads the npm-style top-level `"overrides"`.) The pin is per-version: after upgrading `@prisma/composer`, re-read the package's `dependencies.effect` and update the override to match, or the next Composer command stops on the version conflict.

> [!NOTE]
> Bun workspaces
> 
> In a Bun workspace (a monorepo with isolated installs), two of Composer's indirect dependencies must also be direct ones, because Bun does not expose transitive packages or their bins to your app:
> 
> ```bash
> bun add alchemy
> bun add -d @prisma/dev
> ```
>
> Without `alchemy`, `dev` and `deploy` stop at `DEPLOY.ALCHEMY_BIN_MISSING`; without `@prisma/dev`, local dev stops with "local dev needs @prisma/dev". A single-package Bun project is not affected.

Add a `tsconfig.json` so the compiler checks your wiring the same way the deploy will:

```json title="tsconfig.json"
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Preserve",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "strict": true,
    "skipLibCheck": true,
    "types": ["bun"]
  },
  "include": ["module.ts", "src"]
}
```

This is what you are about to create:

```text no-copy
my-app/
├── module.ts                  # the root module: the app itself
├── prisma-composer.config.ts  # deploy config (read only by the CLI)
└── src/
    ├── quotes/
    │   ├── contract.ts        # the quotes service's public API, as types
    │   ├── service.ts         # what quotes is: deps + build + what it exposes
    │   └── server.ts          # the code that actually runs
    └── gateway/
        ├── service.ts
        └── server.ts
```

## 2. Create the quotes service [#2-create-the-quotes-service]

A service is three files. First, the **contract**: the API other services will call, written as schemas. It lives with the service that owns it. Any [Standard Schema](https://standardschema.dev) validator works (arktype, zod, valibot); these examples use arktype:

```ts title="src/quotes/contract.ts"
import { contract, rpc } from '@prisma/composer/service-rpc';
import { type } from 'arktype';

export const quotesContract = contract({
  random: rpc({ input: type({}), output: type({ quote: 'string' }) }),
});
```

Second, the **service declaration**. This is pure data with no behavior. It says what the service is called, what it depends on (nothing yet), how it is built, and which contract it exposes:

```ts title="src/quotes/service.ts"
import node from '@prisma/composer/node';
import { compute } from '@prisma/composer-prisma-cloud';
import { quotesContract } from './contract.ts';

export default compute({
  name: 'quotes',
  deps: {},
  build: node({ module: import.meta.url, entry: '../../dist/quotes/server.mjs' }),
  expose: { rpc: quotesContract },
});
```

Third, the **server**: the code your build turns into `dist/quotes/server.mjs` and the platform boots. `serve()` generates the HTTP handler from the contract. If you forget a handler or return the wrong shape, the code does not compile:

```ts title="src/quotes/server.ts"
import { serve } from '@prisma/composer/service-rpc';
import service from './service.ts';

const port = service.port(); // the reserved port, resolved (default 3000)

const QUOTES = [
  'Simplicity is prerequisite for reliability.',
  'Make it work, make it right, make it fast.',
];

const handler = serve(service, {
  rpc: {
    random: async () => ({ quote: QUOTES[Math.floor(Math.random() * QUOTES.length)]! }),
  },
});
export default handler;

// Bind all interfaces. Compute routes external HTTP to the VM, so a
// loopback-only listener would be unreachable.
Bun.serve({ port, hostname: '0.0.0.0', fetch: handler });
```

Notice what is missing: no URL of anything and no `process.env`. Every service gets a port for free, read through `service.port()`. Dependencies arrive through `service.load()`. Any configuration of your own arrives through `service.input()` (see [Service input](https://www.prisma.io/docs/composer/service-input)). Those three typed accessors are the whole framework contract with your code.

## 3. Create the gateway service [#3-create-the-gateway-service]

The gateway depends on the quotes contract. Note the asymmetry with step 2: the quotes service *exposed* the bare contract (what it provides), while the gateway wraps it in `rpc()` (what it needs: a client of this contract). Declaring `deps: { quotes: rpc(quotesContract) }` means `service.load()` hands the server a ready-made typed client:

```ts title="src/gateway/service.ts"
import node from '@prisma/composer/node';
import { rpc } from '@prisma/composer/service-rpc';
import { compute } from '@prisma/composer-prisma-cloud';
import { quotesContract } from '../quotes/contract.ts';

export default compute({
  name: 'gateway',
  deps: { quotes: rpc(quotesContract) },
  build: node({ module: import.meta.url, entry: '../../dist/gateway/server.mjs' }),
});
```

```ts title="src/gateway/server.ts"
import service from './service.ts';

const { quotes } = service.load();
const port = service.port();

Bun.serve({
  port,
  hostname: '0.0.0.0',
  fetch: async () => {
    const { quote } = await quotes.random({});
    return new Response(quote);
  },
});
```

The gateway exposes no contract of its own, so there is no `serve()` here. It is an ordinary HTTP server that happens to receive a typed client, and calling `quotes.random({})` is an ordinary async function call.

## 4. Compose the app [#4-compose-the-app]

The root module is the app. It provisions both services and wires the quotes service's exposed port into the gateway's dependency slot. `provision()` returns a ref carrying one port per exposed contract, so `quotes.rpc` exists because the service declared `expose: { rpc: ... }`:

```ts title="module.ts"
import { module } from '@prisma/composer';
import gatewayService from './src/gateway/service.ts';
import quotesService from './src/quotes/service.ts';

export default module('my-app', ({ provision }) => {
  const quotes = provision(quotesService);
  provision(gatewayService, { deps: { quotes: quotes.rpc } });
});
```

Next to it goes the deploy config. Only the Composer CLI reads this file; your app code never imports it:

```ts title="prisma-composer.config.ts"
import { defineConfig } from '@prisma/composer/config';
import { nodeBuild } from '@prisma/composer/node/control';
import { prismaCloud, prismaState } from '@prisma/composer-prisma-cloud/control';

export default defineConfig({
  extensions: [prismaCloud(), nodeBuild()],
  state: prismaState(),
});
```

## 5. Build [#5-build]

You own the build. The framework only assembles what you built, and it has one requirement: each entry must be a single self-contained file, with everything inlined except the runtime's own built-ins (`bun`, `bun:*`, `node:*`). Any bundler that produces such a file works. This guide uses Bun. Build the two services separately, because a multi-entry build would split the shared contract code into a chunk neither output contains:

```json title="package.json"
{
  "scripts": {
    "build": "bun build src/quotes/server.ts --target=bun --outfile dist/quotes/server.mjs && bun build src/gateway/server.ts --target=bun --outfile dist/gateway/server.mjs"
  }
}
```

  

#### bun

```bash
bun run build
```

#### pnpm

```bash
pnpm run build
```

#### yarn

```bash
yarn build
```

#### npm

```bash
npm run build
```

This is also a good moment to typecheck. The compiler checks the whole wiring, so a dependency wired to the wrong producer or a missing RPC handler fails here, in seconds:

  

#### bun

```bash
bunx tsc --noEmit
```

#### pnpm

```bash
pnpm tsc --noEmit
```

#### yarn

```bash
yarn tsc --noEmit
```

#### npm

```bash
npx tsc --noEmit
```

## 6. Run it locally [#6-run-it-locally]

One command brings the whole app up on your machine, both services wired together, with no cloud credentials:

  

#### bun

```bash
bunx prisma@latest dev module.ts
```

#### pnpm

```bash
pnpm dlx prisma@latest dev module.ts
```

#### yarn

```bash
yarn dlx prisma@latest dev module.ts
```

#### npm

```bash
npx prisma@latest dev module.ts
```

It runs the same pipeline a deploy runs, against local stand-ins for Prisma Compute and Prisma Postgres, and prints each service's local URL:

```text no-copy
dev: ready
gateway: http://localhost:3001
quotes: http://localhost:3000
```

Verify it works by calling the gateway:

```bash
curl localhost:3001
```

```text no-copy
Make it work, make it right, make it fast.
```

That response traveled over one typed RPC hop: the gateway received your request, called `quotes.random({})` through its injected client, and returned the result.

`dev` keeps running, watches your built output, and restarts a service when its build changes (run `npm run build` in another terminal and the affected service restarts). `Ctrl-C` stops it. Your local data stays, so the next start is warm. See [Local development](https://www.prisma.io/docs/composer/local-development) for logs, `--fresh`, and what persists.

Now try the quotes service directly:

```bash
curl -X POST localhost:3000/rpc/random -H 'content-type: application/json' -d '{}'
```

You get `401 Unauthorized`. That is deliberate: Composer mints a **service key** for each dependency edge and the quotes service accepts only calls from the gateway. Neither service's code mentions a key. See [Services and contracts](https://www.prisma.io/docs/composer/services-and-contracts#calls-are-authenticated-for-you) for how this works.

## 7. Deploy it (optional) [#7-deploy-it-optional]

Local dev needed no account. Deploying the same app to [Prisma Compute](https://www.prisma.io/docs/compute) needs a signed-in session:

  

#### bun

```bash
bunx prisma@latest auth login
bunx prisma@latest deploy module.ts
```

#### pnpm

```bash
pnpm dlx prisma@latest auth login
pnpm dlx prisma@latest deploy module.ts
```

#### yarn

```bash
yarn dlx prisma@latest auth login
yarn dlx prisma@latest deploy module.ts
```

#### npm

```bash
npx prisma@latest auth login
npx prisma@latest deploy module.ts
```

(In CI, where the browser sign-in is not an option, set `PRISMA_SERVICE_TOKEN` and `PRISMA_WORKSPACE_ID` from the [Prisma Console](https://console.prisma.io/?utm_source=docs\&utm_medium=content\&utm_content=composer) instead; see [Deploying](https://www.prisma.io/docs/composer/deploying#credentials).)

The CLI creates a project named `my-app` in your workspace (a project this module deployed before is reused; one whose hosted state the CLI cannot verify stops the deploy with `HostedStateBootstrapError`, so pass `--name <unique-name>` in that case), provisions both services on Prisma Compute, points the gateway's `quotes` dependency at the deployed quotes service, and starts everything. The deploy finishes by printing what it made: your module names, the platform resource each became, and the public URLs. Open the gateway's URL and you get a quote.

Re-deploying is idempotent. For an isolated copy of the whole app, deploy a stage:

  

#### bun

```bash
bunx prisma@latest deploy module.ts --stage demo
```

#### pnpm

```bash
pnpm dlx prisma@latest deploy module.ts --stage demo
```

#### yarn

```bash
yarn dlx prisma@latest deploy module.ts --stage demo
```

#### npm

```bash
npx prisma@latest deploy module.ts --stage demo
```

Tearing a stage down is the `destroy` operation of the [control API](https://www.prisma.io/docs/cli/deploy#the-control-api). That operation does not read your `auth login` session: it needs `PRISMA_SERVICE_TOKEN` and `PRISMA_WORKSPACE_ID` in the environment, the same service-token credentials CI uses. Without them, delete the stage's branch in the [Console](https://console.prisma.io/?utm_source=docs\&utm_medium=content\&utm_content=composer) instead. See [Deploying](https://www.prisma.io/docs/composer/deploying) for stages, CI, and teardown in full.

## Where to go next [#where-to-go-next]

* [Apps and Modules](https://www.prisma.io/docs/composer/apps-and-modules): package a service and its database as a reusable Module.
* [Databases](https://www.prisma.io/docs/composer/databases): add a Postgres, plain or typed by a Prisma 8 contract.
* [Building blocks](https://www.prisma.io/docs/composer/building-blocks): compose the ready-made cron, storage, and streams Modules.
* [Testing](https://www.prisma.io/docs/composer/testing): unit tests with `mockService`, integration tests with `bootstrapService`.
* [Porting an existing app](https://www.prisma.io/docs/composer/porting-an-app): keep your server code and declare the app around it.

## Related pages

- [`Apps and Modules`](https://www.prisma.io/docs/composer/apps-and-modules): How services, resources, and Modules compose into a Prisma App, and how provision() wires them together.
- [`Building blocks`](https://www.prisma.io/docs/composer/building-blocks): Compose the ready-made cron, storage, and streams Modules instead of building scheduled jobs, blob storage, or event streams yourself.
- [`Core concepts`](https://www.prisma.io/docs/composer/core-concepts): The ideas every Composer declaration and command builds on: services, resources, Modules, ports, contracts, stages, and the deploy model.
- [`Databases`](https://www.prisma.io/docs/composer/databases): Give a service a Postgres database, either as a plain connection or typed by a Prisma 8 contract with managed migrations.
- [`Deploying`](https://www.prisma.io/docs/composer/deploying): Deploy a Prisma App to production or an isolated stage, run it in CI, and tear environments down safely.