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

Add an Orders endpoint

Add POST /orders to the certified minimal starter, then change pricing and verify validation.

Add POST /orders to the application from Create and run an API. You’ll calculate an order’s price, change the bulk-price rule, and check that invalid quantities are rejected.

1. Add the Orders domain

From the application root, scaffold the two functions and their service:

bun run relkit add function price-order --create-service orders
bun run relkit add function create-order --service orders

The first command creates src/services/orders/service.ts and functions/price-order.function.ts beneath that service. The second adds functions/create-order.function.ts and exposes createOrder through service.ts. Both functions start with placeholder schemas and handlers.

Replace the generated files with the examples below. If you prefer to create files manually, add the same paths and contents instead of running these commands. If a file already exists, edit it; relkit add does not overwrite it.

A function contains application behavior. Its input and output schemas describe which values the function accepts and returns. Start with the function that calculates a price:

/** Calculates integer order totals at 100 cents per item for the create-order function. */
import { defineFunction } from "@relkit/app/functions";
import { z } from "@relkit/app/schema";

const priceOrder = defineFunction({
  input: z.object({ quantity: z.number().int().positive() }),
  output: z.object({ totalCents: z.number().int().nonnegative() }),
  handler: ({ quantity }) => ({ totalCents: quantity * 100 }),
});

export default priceOrder;
/** 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;

A service exposes the functions other parts of your application can call. Check that the generated Orders service matches this source:

/** 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 },
});

Now scaffold the HTTP route. --map POST=createOrder selects the public service member:

bun run relkit add route /orders \
  --mode service-route --service orders --map POST=createOrder

This creates src/routes/orders/route.ts with defineServiceRoutes. Replace it with the source below to return HTTP 201 when an order is created; the scaffold’s shorthand uses HTTP 200.

/** 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 },
});

The running development session validates the new source and activates it only after the generated route returns the expected response.

The CLI module explains service selection and other artifacts. Use the function command reference and route command reference for all options.

2. Follow the request

The path and HTTP method come from src/routes/orders/route.ts. The route selects the public createOrder operation and returns 201 after it succeeds. The service exposes createOrder and priceOrder from the Orders domain.

createOrder validates orderId, sku, and a positive integer quantity, then invokes priceOrder. That function owns the price rule. The route stays small so the same behavior can serve other callers.

POST /orders → route → Orders service → createOrder → priceOrder → validated JSON

3. Change one rule

Open src/services/orders/functions/price-order.function.ts. Replace its handler with the source below. Orders of 10 or more units now cost 90 cents each; smaller orders still cost 100 cents each. The input and output schemas remain unchanged.

/** Calculates tutorial totals at 100 cents per item, applying the 90-cent bulk price from ten items onward. */
import { defineFunction } from "@relkit/app/functions";
import { z } from "@relkit/app/schema";

const priceOrder = defineFunction({
  input: z.object({ quantity: z.number().int().positive() }),
  output: z.object({ totalCents: z.number().int().nonnegative() }),
  handler: ({ quantity }) => ({ totalCents: quantity * (quantity >= 10 ? 90 : 100) }),
});

export default priceOrder;

The development server checks and reloads this source. Verify the changed price:

curl -i -X POST http://localhost:3000/orders \
  -H 'content-type: application/json' \
  -d '{"orderId":"bulk-1","sku":"book","quantity":10}'

Expect HTTP 201 and totalCents: 900. Then send an invalid quantity:

curl -i -X POST http://localhost:3000/orders \
  -H 'content-type: application/json' \
  -d '{"orderId":"bad-1","sku":"book","quantity":0}'

Expect a validation response, not an order. Run bun run check and bun run typecheck after the change. check verifies the discovered route and function contracts; typecheck verifies TypeScript.

The response still disappears after each request because this chapter has no database.

Reference and source