Project Layout
ZiWound is a monorepo with two apps: a Deno + Lesan backend (back/) and a Next.js frontend (front/). This page walks the layout of the backend โ the part written with Lesan โ and shows how mod.ts wires everything together. Source: back/ on GitHub.
ziwound/
โโโ back/ # Deno + Lesan backend
โ โโโ mod.ts # entry: lesan(), setDb, register models, runServer
โ โโโ deno.json # tasks + import aliases (@model, @lib, lesan)
โ โโโ deps.ts # centralized dependency re-exports
โ โโโ models/ # one file per model: pure + relations + factory
โ โโโ src/ # one folder per model domain, one folder per act
โ โโโ utils/ # shared helpers, aliased as @lib
โ โโโ declarations/ # generated selectInp.ts (do not hand-edit)
โ โโโ uploads/ # static file uploads
โโโ front/ # Next.js 16 frontend (App Router)
โโโ src/app/ # [locale]/ routes for 9 languages
โโโ src/actions/ # Next.js Server Actions โ Lesan acts
โโโ messages/ # next-intl translation files
The entry point (back/mod.ts)โ
Everything starts here. lesan() creates the app, setDb attaches MongoDB, each model factory registers a model, functionsSetup() registers every act, and runServer boots the HTTP server. Source: back/mod.ts.
import { lesan, MongoClient } from "lesan";
import {
blogPostModel, categories, cities, confirmations, countries,
createBlogPostTextIndex, createUserTextIndex, documents, files,
heroSlides, provinces, reports, tags, users, warCriminals,
} from "@model";
import { functionsSetup } from "./src/mod.ts";
import { RateLimiter } from "./utils/rateLimiter.ts";
const MONGO_URI = Deno.env.get("MONGO_URI") || "mongodb://127.0.0.1:27017/";
const APP_PORT = Deno.env.get("APP_PORT") || 1406;
const ENV = Deno.env.get("ENV") || "development";
export const coreApp = lesan();
const client = await new MongoClient(MONGO_URI).connect();
const db = client.db("gozaresh");
coreApp.odm.setDb(db);
// One exported ODM handle per model:
export const user = users();
export const country = countries();
export const province = provinces();
export const city = cities();
export const tag = tags();
export const category = categories();
export const report = reports();
export const document = documents();
export const blogPost = blogPostModel();
export const heroSlide = heroSlides();
export const file = files();
export const warCriminal = warCriminals();
export const confirmation = confirmations();
export const rateLimiter = new RateLimiter(100, 60 * 1000); // 100 req/min
export const { setAct, setService, getAtcsWithServices } = coreApp.acts;
export const { selectStruct, getSchemas } = coreApp.schemas;
functionsSetup();
createUserTextIndex(); // full-text index for user search
createBlogPostTextIndex(); // full-text index for blog post search
coreApp.runServer({
port: Number(APP_PORT),
typeGeneration: true,
playground: ENV === "development" ? true : false,
staticPath: ["/uploads"],
cors: [
"http://localhost:3000",
"http://localhost:3005",
"http://194.5.192.166:3005",
"http://localhost:4000",
"http://185.204.170.27:4000",
"http://185.204.170.27:3005",
],
});
Model registration order matters
Each newModel call registers one schema and then rebuilds the inverse (relatedRelations) map for every schema. ZiWound keeps all registrations together in mod.ts, in dependency order โ the same convention as the Procurement Workflow app.
Import aliases (back/deno.json)โ
ZiWound uses Deno import aliases so imports stay clean:
{
"imports": {
"lesan": "jsr:@hemedani/lesan",
"mongodb": "npm:mongodb",
"@model": "./models/mod.ts",
"@lib": "./utils/mod.ts"
}
}
lesanโ the framework (viajsr:on Deno; npm/Bun would use@hemedani/lesan)@modelโ the model barrel (pure fields, relations, factories,excludes)@libโ the shared utils barrel
One file per modelโ
A model file declares its pure fields, its relations, and a factory that registers it. Here's a simplified look at the pattern โ full source in back/models/:
// back/models/tag.ts
export const tag_pure = { name: string(), description: optional(string()) };
export const tag_relations = { registrar: { schemaName: "user", type: "single", optional: true } };
export const tags = () => coreApp.odm.newModel("tag", tag_pure, tag_relations);
Relations reference other models by schemaName, and relatedRelations automatically creates the reverse side. The Report model is the deepest example.
One folder per actโ
Every act is a folder with three files โ the registration (mod.ts), the implementation (<act>.fn.ts), and the validator (<act>.val.ts):
src/report/add/
โโโ mod.ts # coreApp.acts.setAct({ schema, actName, fn, validator, preAct })
โโโ add.fn.ts # the ActFn implementation
โโโ add.val.ts # the superstruct validator (set / get)
Each domain exports a *Setup(); functionsSetup() in src/mod.ts calls them all:
export const functionsSetup = () => {
countrySetup(); citySetup(); fileSetup(); provinceSetup(); userSetup();
tagSetup(); categorySetup(); reportSetup(); documentSetup();
blogPostSetup(); heroSlideSetup(); warCriminalSetup(); confirmationSetup();
};
Frontend bridgeโ
The frontend never writes raw fetch calls. It uses Next.js Server Actions that call the Lesan acts โ see front/src/actions/ โ and reads the generated types from the backend's declarations/. LESAN_URL / NEXT_PUBLIC_LESAN_URL env vars point the frontend at the backend.
Next: The Models.