- TypeScript 97.4%
- JavaScript 2.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| src | ||
| test | ||
| .gitignore | ||
| bunfig.toml | ||
| eslint.config.mjs | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.test.json | ||
| tsup.config.ts | ||
| vitest.config.ts | ||
@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/coreknows 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-inJSONSchema.make()does the work
License
GPL-3.0-only