No description
  • TypeScript 97.4%
  • JavaScript 2.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-05-26 21:49:13 +02:00
src Expose entity descriptor through api 2026-05-26 21:49:13 +02:00
test Project layout and basic server functionality working 2026-05-26 20:22:57 +02:00
.gitignore Project layout and basic server functionality working 2026-05-26 20:22:57 +02:00
bunfig.toml Project layout and basic server functionality working 2026-05-26 20:22:57 +02:00
eslint.config.mjs Project layout and basic server functionality working 2026-05-26 20:22:57 +02:00
LICENSE Initial commit 2026-05-24 18:33:23 +02:00
package.json Project layout and basic server functionality working 2026-05-26 20:22:57 +02:00
README.md Project layout and basic server functionality working 2026-05-26 20:22:57 +02:00
tsconfig.json Project layout and basic server functionality working 2026-05-26 20:22:57 +02:00
tsconfig.test.json Project layout and basic server functionality working 2026-05-26 20:22:57 +02:00
tsup.config.ts Project layout and basic server functionality working 2026-05-26 20:22:57 +02:00
vitest.config.ts Project layout and basic server functionality working 2026-05-26 20:22:57 +02:00

@tesserakit/server

Transport layer for TesseraKit — Effect RPC, REST/JSON (Hono), auth middleware, and OpenAPI generation.

Built on top of @tesserakit/core, this package maps domain errors to HTTP responses, wires entity services to Hono routes, exposes Effect RPC endpoints, and generates OpenAPI 3.1 specs — all with a DDD-friendly, tree-shakeable API.

Installation

bun add @tesserakit/server hono effect @tesserakit/core

Modules

Tree-shakeable sub-path imports:

Import Contents
@tesserakit/server/errors HTTP error types + DomainError → HTTP mapper
@tesserakit/server/auth Actor Effect context + JWT/session middleware
@tesserakit/server/http REST router factory, body/query validators, OpenAPI generator
@tesserakit/server/rpc Effect RPC procedure factory + Hono adapter

@tesserakit/server/errors

Maps @tesserakit/core domain errors to HTTP-layer tagged errors and serialises them into Hono responses.

import { mapDomainError, sendHttpError } from "@tesserakit/server/errors";
import { EntityNotFoundError } from "@tesserakit/core/errors";

// Map a domain error to an HTTP error
const httpError = mapDomainError(new EntityNotFoundError({ entity: "Person", id }));
// → HttpNotFoundError { _tag: "HttpNotFoundError", message: "...", status: 404 }

// Write the error as a JSON response in a Hono handler
app.onError((err, c) => sendHttpError(c, mapDomainError(err as never)));

Domain → HTTP status mapping

Domain Error HTTP Error Status
EntityNotFoundError HttpNotFoundError 404
EntityConflictError HttpConflictError 409
EntityValidationError HttpUnprocessableError 422
RepositoryError HttpInternalError 500
UnauthorizedError HttpUnauthorizedError 401
ForbiddenError HttpForbiddenError 403

@tesserakit/server/auth

Actor context (Effect)

import { Actor, ActorLayer } from "@tesserakit/server/auth";
import { Effect } from "effect";

// Declare a dependency on the current actor in any Effect
const myHandler = Effect.gen(function* () {
  const actor = yield* Actor;
  console.log("Request from:", actor.id, "roles:", actor.roles);
});

// Provide the actor via a Layer
const layer = ActorLayer({ id: actorUrn, roles: ["editor"] });
Effect.provide(myHandler, layer);

JWT middleware

import { withJwt, ACTOR_KEY } from "@tesserakit/server/auth";
import { Hono } from "hono";

const app = new Hono();

app.use("/api/*", withJwt({
  verify: async (token) => {
    // Verify token — return ActorFields or null
    const payload = await myJwtVerify(token);
    if (!payload) return null;
    return { id: payload.sub as UrnType, roles: payload.roles };
  },
}));

app.get("/api/me", (c) => {
  const actor = c.var[ACTOR_KEY]; // ActorFields
  return c.json({ id: actor.id });
});

Session middleware

import { withSession } from "@tesserakit/server/auth";

app.use("/api/*", withSession({
  cookieName: "sid",          // default: "session"
  resolve: async (sessionId) => sessionStore.get(sessionId),
}));

@tesserakit/server/http

CRUD router factory

Give it a CrudSchemas object and a service implementation — get a fully wired Hono router back.

import { createCrudRouter } from "@tesserakit/server/http";
import { PersonCrud } from "./person.schema";
import type { CrudService } from "@tesserakit/server/http";
import { Effect } from "effect";

// Implement the service
const personService: CrudService<Person> = {
  findMany:  (input)     => Effect.tryPromise(() => db.findPersons(input)),
  findById:  (id)        => Effect.tryPromise(() => db.findPerson(id)),
  create:    (input)     => Effect.tryPromise(() => db.createPerson(input)),
  update:    (id, data)  => Effect.tryPromise(() => db.updatePerson(id, data)),
  delete:    (id)        => Effect.tryPromise(() => db.deletePerson(id)),
};

// Wire it to a Hono router
const personRouter = createCrudRouter(PersonCrud, personService, {
  middleware: [withJwt({ verify: myVerify })],    // applied to all routes
  writeMiddleware: [],                             // applied to POST/PATCH/DELETE only
});

app.route("/persons", personRouter);
// → GET /persons, GET /persons/:id, POST /persons, PATCH /persons/:id, DELETE /persons/:id

Request validation middleware

import { validateBody, validateQuery } from "@tesserakit/server/http";
import { PersonCrud } from "./person.schema";

app.post("/persons",
  validateBody(PersonCrud.CreateInput, "createInput"),
  (c) => {
    const input = c.var.createInput; // fully typed
    // ...
  }
);

app.get("/persons",
  validateQuery(PersonCrud.FindManyInput, "query"),
  (c) => {
    const query = c.var.query;
    // ...
  }
);

OpenAPI generation

import { generateOpenApiSpec, schemaToJsonSchema } from "@tesserakit/server/http";
import { PersonCrud } from "./person.schema";

const spec = generateOpenApiSpec({
  info: { title: "My API", version: "1.0.0", description: "TesseraKit-powered API" },
  bearerAuth: true,   // adds securitySchemes + security to every operation
  entities: [
    {
      path: "/persons",
      name: "Person",
      schemas: {
        entity:            schemaToJsonSchema(PersonCrud.Entity),
        createInput:       schemaToJsonSchema(PersonCrud.CreateInput),
        updateData:        schemaToJsonSchema(PersonCrud.UpdateInput),
        findManyInput:     schemaToJsonSchema(PersonCrud.FindManyInput),
        paginatedResponse: schemaToJsonSchema(PersonCrud.PaginatedResponse),
      },
    },
  ],
});

app.get("/openapi.json", (c) => c.json(spec));

@tesserakit/server/rpc

Effect RPC enables fully typed, schema-validated remote procedures. The client and server share the same procedure definitions — no hand-written HTTP routes or manual JSON parsing.

Define procedures from CrudSchemas

import { createCrudProcedures, createRpcGroup, buildRpcHandler } from "@tesserakit/server/rpc";
import { PersonCrud } from "./person.schema";

// 1. Create typed procedure definitions
const PersonProcedures = createCrudProcedures(PersonCrud, "Person");
// → Person.FindMany, Person.FindById, Person.Create, Person.Update, Person.Delete

// 2. Group them into an RpcGroup
const PersonGroup = createRpcGroup(PersonProcedures);

// 3. Implement the handlers
const personHandlers = PersonGroup.of({
  "Person.FindMany": ({ filters, pagination }) =>
    Effect.tryPromise(() => db.findPersons({ filters, pagination })),
  "Person.FindById": ({ id }) =>
    Effect.tryPromise(() => db.findPerson(id)),
  "Person.Create":   (input) =>
    Effect.tryPromise(() => db.createPerson(input)),
  "Person.Update":   ({ id, data }) =>
    Effect.tryPromise(() => db.updatePerson(id, data)),
  "Person.Delete":   ({ id }) =>
    Effect.tryPromise(() => db.deletePerson(id)),
});

const personHandlerLayer = PersonGroup.toLayer(personHandlers);

// 4. Mount as a Hono handler
const rpcFetchHandler = buildRpcHandler(PersonGroup, personHandlerLayer);

app.post("/rpc/persons", (c) => rpcFetchHandler(c.req.raw));

Design principles

  • Effect-first — service methods are Effects; errors flow through the Effect channel
  • DDD-friendly — small, focused files; clean separation of errors / auth / http / rpc
  • Transport-agnostic core — @tesserakit/core knows nothing about HTTP; this package owns the mapping
  • Tree-shakeable — import only what you need via sub-path exports
  • OpenAPI from Effect Schema — no Zod adapter; effect's built-in JSONSchema.make() does the work

License

GPL-3.0-only