Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

Your first service

Trace the Orders extension's function through its public service and HTTP route.

Trace the Orders endpoint through its route, service, and pricing function to see where each responsibility belongs. You will identify which file owns pricing, which operations the Orders service exposes, and how a route reaches them. That chapter adds these maintained API-candidate sources to the certified minimal starter; it does not require an uncertified creation tuple. Run commands from the app root.

The endpoint chapter uses relkit add function --create-service to create the service and its first operation together. To start a different domain, use relkit add service; to extend this one, use relkit add function --service orders. Keep the existing Orders files while following this walkthrough.

Keep behavior in the Orders domain

src/services/orders/functions/create-order.function.ts validates an order request and asks priceOrder to calculate the total. It does not parse an HTTP request:

/** Validates order input, invokes the service-local pricing function, and returns the calculated order. */
import { defineFunction } from "@relkit/app/functions";
import { z } from "@relkit/app/schema";
import priceOrder from "@app/services/orders/functions/price-order.function.js";

const orderInput = z.object({
  orderId: z.string().min(1),
  sku: z.string().min(1),
  quantity: z.number().int().positive(),
});

const orderOutput = z.object({
  orderId: z.string(),
  sku: z.string(),
  totalCents: z.number().int().nonnegative(),
});

const createOrder = defineFunction({
  input: orderInput,
  output: orderOutput,

  handler: async (input) => {
    const { totalCents } = await priceOrder.invoke({ quantity: input.quantity });
    const order = {
      orderId: input.orderId,
      sku: input.sku,
      totalCents,
    };

    return order;
  },
});

export default createOrder;

The function can be called from HTTP, a job, or another function. Its input and output schemas apply on every invocation. Create tests/unit/orders.function.test.ts with this focused candidate test to invoke it without starting a server:

/** Checks orders.function through the supported public RELKIT APIs. */
import { expect, test } from "bun:test";
import { invokeFunction } from "@relkit/testing";
import orders from "@app/services/orders/service.js";

test("createOrder invokes pricing", async () => {
  const result = await invokeFunction(orders.createOrder, {
    orderId: "order-1",
    sku: "book",
    quantity: 3,
  });

  expect(result).toEqual({ orderId: "order-1", sku: "book", totalCents: 300 });
});

Expose only selected operations

The existing src/services/orders/service.ts selects the functions that other parts of the app may call. priceOrder and createOrder remain separate operations, so changing a price rule does not require moving it into a route:

/** Declares the orders service boundary and its public members. */
import { defineService } from "@relkit/app/services";
import createOrder from "@app/services/orders/functions/create-order.function.js";
import priceOrder from "@app/services/orders/functions/price-order.function.js";

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

The compiler discovers this service from source. You do not add it to a registration array. The route imports the service and selects createOrder for POST /orders:

/** Maps the HTTP endpoint to its owning service's public member. */
import { defineServiceRoutes } from "@relkit/app/routes";
import orders from "@app/services/orders/service.js";

export const { POST } = defineServiceRoutes(orders, {
  POST: { member: "createOrder", successStatus: 201 },
});

Exposing a service member does not itself create an endpoint or grant caller permission. The route provides HTTP exposure; the auth chapter later enforces identity and ownership.

Verify the boundary

From relkit-orders, run:

Create tests/integration/orders.route.test.ts from the HTTP route test before running its command below. These tests are Orders extensions, not files shipped by minimal.

bun run check
bun run typecheck
bun test tests/unit/orders.function.test.ts
bun test tests/integration/orders.route.test.ts

Expect all four commands to succeed after adding the Orders sources and tests. The unit test checks the reusable operation; the HTTP test checks the public route’s status and body. Continue with the endpoint chapter to change pricing and verify the result.

Reference and source