---
title: Save and retrieve orders
description: Add a local SQLite database, persist POST /orders, and read a saved order after restart.
sidebar:
  icon: database
---

The Orders route from [the previous chapter](/docs/learn/first-route) only calculates a response.
Here you will store that result in SQLite and add `GET /orders/:orderId`. Stop `bun run dev`
before changing the database. Keep working in the same `relkit-orders` application.

1. ## 1. Add the local database [#add-the-local-database]

    Scaffold the supported Drizzle service. This installs the compatible packages and creates an
    `items` placeholder. **Replace** that placeholder with the Orders schema below; do not keep both.

    ```sh title="Terminal — relkit-orders"
    bun run relkit add database --orm drizzle --dialect sqlite
    ```

    This also installs Drizzle Kit and registers the release-owned Drizzle declaration repair. If you
    pass `--no-install`, complete `bun install` and `bun run check` before the next step. For a manual
    alternative, follow [database installation](/docs/modules/database/define); do not run both setup paths.

    Replace `src/services/database/schema/index.ts` with one Orders table. `orderId` is the unique key;
    `ownerId` is empty until the next chapter adds sign-in. Existing anonymous demonstration orders
    will not become anyone's private orders automatically.

    ```ts title="src/services/database/schema/index.ts"
    /** Declares the tutorial's SQLite order identity, quantity, price and optional ownership columns for native registration. */
    import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";

    export const orders = sqliteTable("orders", {
      orderId: text().primaryKey(),
      sku: text().notNull(),
      quantity: integer().notNull(),
      totalCents: integer().notNull(),
      ownerId: text(),
    });
    ```

    Use Bun's SQLite driver in `src/services/database/service.ts`. Replace the scaffolded service with:

    ```ts title="src/services/database/service.ts"
    /** Lazily opens the configured SQLite database, registers tutorial tables and closes its owned client on disposal. */
    import { Database } from "bun:sqlite";
    import { defineDrizzleService } from "@relkit/drizzle";
    import { drizzle } from "drizzle-orm/bun-sqlite";
    import * as schema from "./schema/index.js";

    export default defineDrizzleService({
      schema,
      client: ({ env }) => drizzle({ client: new Database(env.DATABASE_PATH) }),
      dispose: (database) => database.$client.close(),
    });
    ```

    Replace `src/platform/env.ts` to keep the existing log-level setting and add the SQLite file path:

    ```ts title="src/platform/env.ts"
    import { defineEnv, env } from "@relkit/app/config";

    export default defineEnv({
      LOG_LEVEL: env.literal("trace", "debug", "info", "warn", "error").default("info"),
      DATABASE_PATH: env.string().default("./orders.sqlite"),
    });
    ```

    Update `tsconfig.json` so TypeScript loads the context registry produced by `relkit check`.
    Keep the starter's `@app/*` alias; the complete config below adds the generated declaration
    and includes the migration script.

    ```json title="tsconfig.json"
    {
      "$schema": "https://json.schemastore.org/tsconfig",
      "compilerOptions": {
        "target": "ES2022",
        "module": "ESNext",
        "moduleResolution": "Bundler",
        "baseUrl": ".",
        "paths": { "@app/*": ["src/*"] },
        "lib": ["ES2022"],
        "types": ["bun"],
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "exactOptionalPropertyTypes": true,
        "verbatimModuleSyntax": true,
        "isolatedModules": true,
        "noEmit": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
      },
      "files": [".relkit/generated/context-registry.d.ts"],
      "include": [
        "src/**/*.ts",
        "tests/**/*.ts",
        "scripts/**/*.ts",
        "drizzle.config.ts",
        "relkit.config.ts"
      ],
      "exclude": ["node_modules", ".relkit"]
    }
    ```

    Add the SQLite path to the ignored `.env` file, preserving its existing settings. The same path will be loaded when you
    restart the app or run the production build. Keep the database out of source control too.

    ```sh title="Terminal — relkit-orders"
    printf '\nDATABASE_PATH=./orders.sqlite\n' >> .env
    printf '\norders.sqlite\n' >> .gitignore
    ```

2. ## 2. Save an order [#save-an-order]

    The existing `createOrder` function already validates input and calls `priceOrder`. Replace
    `src/services/orders/functions/create-order.function.ts` so it inserts the calculated result into the
    Orders table. The response now includes the original `quantity` as well as the price.

    ```ts title="src/services/orders/functions/create-order.function.ts"
    /** Prices validated order input and persists its selected fields through the tutorial's native database context. */
    import { defineFunction } from "@relkit/app/functions";
    import { z } from "@relkit/app/schema";
    import priceOrder from "@app/services/orders/functions/price-order.function.js";
    const order = z.object({
      orderId: z.string().min(1),
      sku: z.string().min(1),
      quantity: z.number().int().positive(),
    });
    export default defineFunction({
      input: order,
      output: z.object({
        orderId: z.string(),
        sku: z.string(),
        quantity: z.number().int().positive(),
        totalCents: z.number().int().nonnegative(),
      }),
      handler: async (input, context) => {
        const { totalCents } = await priceOrder.invoke({ quantity: input.quantity });
        const saved = await context.database.orders.insert({
          data: { ...input, totalCents, ownerId: null },
        });
        return {
          orderId: saved.orderId,
          sku: saved.sku,
          quantity: saved.quantity,
          totalCents: saved.totalCents,
        };
      },
    });
    ```

3. ## 3. Add a read operation [#add-a-read-operation]

    From the application root, scaffold the read function:

    ```sh title="Terminal — relkit-orders"
    bun run relkit add function get-order --service orders
    ```

    This creates `src/services/orders/functions/get-order.function.ts` and exposes `getOrder` through
    the service. Replace its placeholder with the lookup below. It returns `found: false` when there
    is no row, so callers receive no fabricated order. For manual setup, create the same file instead.

    ```ts title="src/services/orders/functions/get-order.function.ts"
    /** Reads the requested database order and returns validated selected fields or an explicit missing result. */
    import { defineFunction } from "@relkit/app/functions";
    import { z } from "@relkit/app/schema";
    export default defineFunction({
      input: z.object({ orderId: z.string().min(1) }),
      output: z.object({
        found: z.boolean(),
        order: z
          .object({
            orderId: z.string(),
            sku: z.string(),
            quantity: z.number(),
            totalCents: z.number(),
          })
          .optional(),
      }),
      handler: async ({ orderId }, context) => {
        const row = await context.database.orders.findOne({ where: { orderId } });
        return row === null
          ? { found: false }
          : {
              found: true,
              order: {
                orderId: row.orderId,
                sku: row.sku,
                quantity: row.quantity,
                totalCents: row.totalCents,
              },
            };
      },
    });
    ```

    Replace `src/services/orders/service.ts` to expose that operation alongside the existing two functions:

    ```ts title="src/services/orders/service.ts"
    /** Exposes tutorial creation, lookup and pricing functions through the immediate orders service boundary. */
    import { defineService } from "@relkit/app/services";
    import createOrder from "@app/services/orders/functions/create-order.function.js";
    import getOrder from "@app/services/orders/functions/get-order.function.js";
    import priceOrder from "@app/services/orders/functions/price-order.function.js";

    export default defineService({ functions: { createOrder, getOrder, priceOrder } });
    ```

    Scaffold the read route after updating the function and service:

    ```sh title="Terminal — relkit-orders"
    bun run relkit add route /orders/:orderId \
      --mode service-route --service orders --map GET=getOrder
    ```

    This creates `src/routes/orders/[orderId]/route.ts`. Its directory supplies the URL parameter;
    the named `GET` export selects the public `getOrder` member. The generated shorthand and the
    explicit mapping below select the same operation; use this source to match the tutorial:

    ```ts title="src/routes/orders/[orderId]/route.ts"
    /** Maps the order GET route to its service lookup member with inferred path input and validated response. */
    import { defineServiceRoutes } from "@relkit/app/routes";
    import orders from "@app/services/orders/service.js";

    export const { GET } = defineServiceRoutes(orders, { GET: { member: "getOrder" } });
    ```

4. ## 4. Keep existing HTTP tests compatible [#keep-existing-http-tests-compatible]

    Application test fixtures load the app's declared services, even when a test only calls `/hello`.
    After adding the database, replace the starter's generic `createTestApplication` import with
    `@relkit/testing/drizzle` in every application fixture. Keep `invokeFunction` unit tests on the
    generic helper when they inject their own dependencies.

    Update the existing Hello HTTP test:

    ```ts title="tests/integration/hello.route.test.ts"
    /** Keeps the starter's Hello HTTP coverage after the application selects a native Drizzle service. */
    import { afterAll, expect, test } from "bun:test";
    import { createTestApplication } from "@relkit/testing/drizzle";
    import config from "../../relkit.config.js";

    const testApp = await createTestApplication(config);

    test("GET /hello", async () => {
      const response = await testApp.http.get("/hello?name=Mustafa");
      expect(response.status).toBe(200);
      expect(await response.json()).toEqual({ message: "Hello, Mustafa!" });
    });

    afterAll(() => testApp.close());
    ```

5. ## 5. Migrate, run, and check [#migrate-run-and-check]

    Replace the scaffolded `drizzle.config.ts` with this configuration. It refuses an unset or
    in-memory path so the migration cannot quietly target the wrong database.

    ```ts title="drizzle.config.ts"
    /** Configures native tutorial schema migrations against grouped order tables and an explicitly selected SQLite file. */
    import { defineConfig } from "drizzle-kit";

    const path = process.env.DATABASE_PATH;
    if (path === undefined || path.trim() === "" || path === ":memory:") {
      throw new Error("Set DATABASE_PATH to the SQLite file you intend to migrate");
    }

    export default defineConfig({
      schema: "./src/services/database/schema/index.ts",
      dialect: "sqlite",
      out: "./src/services/database/migrations",
      dbCredentials: { url: path },
    });
    ```

    Generate and review the initial SQL migration, then apply it and start the app:

    Use installed RELKIT migration tooling, which selects the Bun-native SQLite driver.
    Keep generated SQL and required metadata tracked beside the database service in
    `src/services/database/migrations`; do not copy runner code into the application.

    ```sh title="Terminal — relkit-orders"
    export DATABASE_PATH=./orders.sqlite
    bun --bun drizzle-kit generate --name=initial
    bun run relkit db migrate
    bun run check
    bun run typecheck
    bun run dev
    ```

    In another terminal, submit a new order and read it back:

    ```sh title="Second terminal — relkit-orders"
    curl -i -X POST http://localhost:3000/orders \
      -H 'content-type: application/json' \
      -d '{"orderId":"saved-1","sku":"book","quantity":10}'
    curl http://localhost:3000/orders/saved-1
    ```

    Expect HTTP `201` with `totalCents: 900`; the GET response has `found: true` and the same
    order. Restart `bun run dev` and repeat the GET: the result remains. Use a new `orderId` for
    another POST because the database enforces uniqueness.

    This chapter's API has no access control yet.

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/drizzle` API](/docs/api/drizzle)
- [`@relkit/functions` API](/docs/api/functions)

- [`examples/data-model/src/services/database/service.ts`](https://github.com/rel-kit/relkit/blob/main/examples/data-model/src/services/database/service.ts)
