Skip to content

Getting Started

Rizky Zulkarnaen edited this page Sep 9, 2026 · 1 revision

Getting Started

Source of truth: docs/getting-started.md. This wiki page mirrors it; the repo doc wins on drift.

Install

bun add lugas@beta

Lugas requires Bun 1.4.x. TypeScript 7.0.2 is the verified toolchain for the full compile-time contract experience.

The v0.1.0-beta.1 candidate is attested but not yet published — npm publication is an explicit owner action. Until it is announced, the package should not be assumed available.

Hello world

// app.ts
import { defineApp, json, route } from "lugas";

const app = defineApp({
  routes: {
    "/hello": {
      GET: route({
        handler: () => json(200, { message: "Hello from Lugas" }),
      }),
    },
  },
});

export default app;
// server.ts
import app from "./app";

const server = app.serve({ port: 3000 });
console.log(`Lugas is listening on ${server.url}`);
bun run server.ts
curl http://localhost:3000/hello

Validation and typed guards

Lugas accepts validators implementing Standard Schema v1 (Zod, Valibot, or any conforming implementation). Validation outputs are typed on the handler context (ctx.body, ctx.query, ctx.params, ctx.headers).

import { defineApp, defineModule, guard, json, route } from "lugas";
import { z } from "zod";

const authGuard = guard({
  name: "auth",
  handler: (ctx) => {
    const authorization = ctx.request.headers.get("authorization");
    if (!authorization) return json(401, { error: "unauthorized" });
    return { user: { id: "usr_123" } };
  },
});

const invoices = defineModule({
  name: "invoices",
  routes: {
    "/invoices": {
      POST: route({
        before: [authGuard],
        body: z.object({
          amount: z.number().positive(),
          currency: z.string().length(3),
        }),
        handler: (ctx) => json(201, {
          id: "inv_123",
          amount: ctx.body.amount,
          currency: ctx.body.currency,
          createdBy: ctx.user.id,
        }),
      }),
    },
  },
});

export default defineApp({ modules: [invoices] });

A guard may either return context (merged in declaration order, available to later guards and the handler) or return a typed Response and short-circuit the route.

End-to-end typed client

import type { AppContract } from "lugas";
import { createClient } from "lugas/client";
import type app from "./app";

type API = AppContract<typeof app>;

const api = createClient<API>({ baseUrl: "https://api.example.com" });

const result = await api.post("/invoices", {
  body: { amount: 125, currency: "USD" },
  headers: { authorization: "Bearer token" },
});

if (result.ok) console.log(result.data.id);
else console.error(result.status, result.error);

Compile-time checks cover supported paths and methods, route parameters, query values, headers, request bodies, and success/error statuses. Explicit method calls and path strings — no runtime Proxy, no generated SDK. Response types model JSON serialization truth ("wire-honest types"): Datestring, non-finite numbers → null | number, toJSON drops modeled.

Without a build step

The package ships a prebuilt browser ESM artifact: lugas/client/browser (build/lugas-client.esm.js). Three consumption arrangements: same-origin serving via assets.files, an import map, or continued bundler use. The artifact lane is same-origin only; for cross-origin frontends pair it with CORS.

Testing

import { expect, test } from "bun:test";
import { createTestServer } from "lugas/testing";
import app from "./app";

test("creates an invoice", async () => {
  const server = createTestServer(app, { port: 0 });
  try {
    const response = await server.fetch("/invoices", {
      method: "POST",
      headers: { authorization: "Bearer test-token", "content-type": "application/json" },
      body: JSON.stringify({ amount: 125, currency: "USD" }),
    });
    expect(response.status).toBe(201);
  } finally {
    await server.stop();
  }
});

CLI

bunx lugas routes ./app.ts    # human-readable route table
bunx lugas inspect ./app.ts   # full lugas-manifest-v1 JSON

Error handling

RFC 9457 Problem Details (application/problem+json) for structured errors:

Condition Status Code
Malformed JSON 400 MALFORMED_JSON
Unsupported media type 415 UNSUPPORTED_MEDIA_TYPE
Validation failure 422 VALIDATION_FAILED
Unhandled internal error 500 redacted — no stack traces or internals reach the client

Typed response helpers: json(status, body), text(status, body), problem(status, fields), empty(status).

Next steps

Clone this wiki locally