Application structure
Every zerolith application keeps the same files in the same places, whether it runs on the zerolith command line or on Vite, ships as one bundle or as modules behind an import map, or renders on the server. A reader, a tool, or an agent that knows one zerolith project knows them all.
What zerolith new writes
zerolith new my-app with every default writes a TypeScript project with JSX that develops and builds on the zerolith command line, with three working pages:
- my-app/
- index.htmlthe page: the stylesheet, the entry module, and #app
- package.jsonscripts: dev, build, preview, check, typecheck, lint, test
- zerolith.jsonthe command line's settings: base path, proxy, dev, build, preview
- tsconfig.jsonstrict TypeScript, with JSX from zerolith
- vitest.config.tsbrowser tests in Chromium, with the zerolith plugin
- .oxlintrc.jsonlint rules
- .gitignoredependencies, the build, and local .env files
- AGENTS.mdthe map and the rules, for coding agents and new contributors
- README.mddeveloping, building, deploying, and environment variables
- .agents/skills/the zerolith skill, for Codex, Cursor, GitHub Copilot, and most agents
- .claude/skills/the same skill, for Claude Code
- public/files served as they are: favicon.svg and data/packages.json
- src/
- main.tswaits for the router, renders the layout, mounts the inspector
- routes.tsthe route table, one entry per page
- router.tscreateRouter over the table, for typed links in components
- styles.cssdesign tokens, resets, and global rules
- env.d.tsthe types of import.meta.env and ?scoped stylesheets
- components/
- layout.tsxthe frame around every page: header, navigation, the page, footer
- home.tsxthe first page: what the project is and its commands
- counter.tsxa page with state: a named signal and a value derived from it
- packages.tsxa page with data: a resource, a search, and a keyed list
- not-found.tsxthe page for a path no route matches
- test/
- counter.browser.test.tsa browser test with @zerolith/test
The other choices change a few files and keep the layout. --template ts and --template js write the components as .ts or .js files with html templates, and a JavaScript project types its sources with JSDoc, which npm run typecheck checks as well. --vite writes a vite.config.ts in place of zerolith.json. Installing the dependencies adds node_modules/ and package-lock.json.
As the application grows
Some files appear only when an application needs them, and they have places of their own too:
| File | Added when | Holds |
|---|---|---|
src/store.ts |
Two components read the same state | Signals, resources, and the operations that change them |
src/schema.ts |
A form validates its input | The Standard Schemas a form checks, which the server imports too |
openapi.json, src/api.ts |
The app calls a backend that publishes an OpenAPI document | The contract, and the typed client zerolith-client generates from it |
messages/, src/messages/, src/locale.ts |
The app speaks more than one language | A catalog per locale, the modules zerolith-i18n compiles from them, and the locale signal with its loaders |
server/, shared/ |
The backend is TypeScript in the same project | The backend, and the types, the API contract, and the schemas both sides import, as in the fullstack example |
What each file owns
main.tsdoes the things that happen once, in order: it waits for what the first paint needs,router.readyand, in an app with messages, the current locale's catalog; it renders the layout aroundrouter.outlet(); and it mounts development-only tools such as the inspector, which the production build replaces with a module that does nothing. Nothing else is global.routes.tsis the single list of pages. Each route names its view and can add a loader and head metadata; the router infers the parameters from each path, so a wrongrouter.hrefcall failstsc. The routing guide builds a small application from these files.router.tscreates the router in a module of its own, so components can import it for typed links andnavigate.store.tsis where state lives once more than one component reads it. State that only one component needs stays inside that component.components/holds functions that return views, one screen or widget per file, each with its styles next to the markup they style: acssliteral in the file, or a stylesheet beside it imported with?scoped, as inimport card from "./card.css?scoped".api.tsandsrc/messages/are generated. They are checked in so the project builds without running the generators, and regenerated withzerolith-clientandzerolith-i18nafter their inputs change.AGENTS.mdsays so, so nobody edits them by hand.schema.tsholds the Standard Schemas a form validates with, so the server can import the same module and run the same checks.
Conventions the tooling relies on
| Convention | Why |
|---|---|
A component is a capitalized function that takes one props object, with children as the children prop |
JSX treats a capitalized tag as a component and calls it with its attributes as the props, and the Vite plugin swaps capitalized exports in place when their module changes |
State that must survive an edit is a named signal: signal(0, { name: "count" }) |
zerolith dev and the Vite plugin restore named signals after an edit; anonymous ones start over |
Pages not needed on first paint use lazy(), and each locale's messages are a module of their own |
The bundler splits them into chunks of their own without configuration |
| Relative imports carry the file extension | The same source runs unbundled, behind an import map |
.get() is called only inside computed, effect, the params of a resource, and event handlers |
A template binds the signal itself; a value read in a template is frozen |
The AGENTS.md
An agent reads one file before touching a project. The scaffold writes this one for zerolith new my-app, and the point of its shape is that it fits in context next to the task:
my-app: guide for coding agents
A zerolith application. The project develops and builds on the zerolith command line; packages load from node_modules with no import map, and there is no Vite config. This file holds what is specific to this project. zerolith's own rules and patterns are in the zerolith skill in .agents/skills/zerolith/, also at .claude/skills/zerolith/ for Claude Code; npx zerolith skill brings it to the installed version after an upgrade. The guides at https://zerolith.dev/guide/ cover each concept in full.
Commands
npm run devserves the app with live reload;npm run buildwritesdist/;npm run previewservesdist/.npm run checkis the gate: typecheck, lint, browser tests, and the build.- With
@zerolith/clientinstalled,npx zerolith-client openapi.json --out src/api.tsregenerates the typed client from the API contract, in TypeScript, which the build serves in a JavaScript project too; with@zerolith/i18ninstalled,npx zerolith-i18n messages --out src/messagesregenerates the message modules.
Rules
- Bind the signal, not its value: a live binding takes
count, nevercount.get(), which freezes. Call.get()only insidecomputed,effect,resource.params, and event handlers. - Components are capitalized functions written in JSX, one per file under
src/components/, with their styles next to the markup, in acssliteral or in a stylesheet beside the component imported with?scoped. JSX is a spelling ofhtmltemplates:class, notclassName;on:clickfor events,prop:valuefor properties,bool:disabledfor boolean attributes; lists and conditions areeachandwhen. - State that must survive an edit is a named signal:
signal(0, { name: "count" }). src/api.tsandsrc/messages/are generated when they exist; change their inputs and regenerate, never edit them.- A new page is a route in
src/routes.tsand a component; shared state goes insrc/store.ts, created with the first piece of it. - Read
routerinside functions only:src/router.tsimports the route table, which imports the components, so a component module that usesrouterat its top level runs before the router exists. - Files served as they are, such as data, icons, and
robots.txt, go inpublic/. - Only
.envvariables namedPUBLIC_*reach the browser, asimport.meta.env.PUBLIC_*.
Where things go
| Change | Location |
|---|---|
| A page | src/routes.ts and src/components/<page>.tsx |
| Shared state or a request | src/store.ts |
A message, once the project uses @zerolith/i18n |
messages/<locale>.json, then regenerate |
| A style token | src/styles.css; component styles stay with the component |
| A static file | public/ |
# my-app: guide for coding agents
A zerolith application. The project develops and builds on the zerolith command line; packages load from `node_modules` with no import map, and there is no Vite config. This file holds what is specific to this project. zerolith's own rules and patterns are in the zerolith skill in `.agents/skills/zerolith/`, also at `.claude/skills/zerolith/` for Claude Code; `npx zerolith skill` brings it to the installed version after an upgrade. The guides at https://zerolith.dev/guide/ cover each concept in full.
## Commands
- `npm run dev` serves the app with live reload; `npm run build` writes `dist/`; `npm run preview` serves `dist/`.
- `npm run check` is the gate: typecheck, lint, browser tests, and the build.
- With `@zerolith/client` installed, `npx zerolith-client openapi.json --out src/api.ts` regenerates the typed client from the API contract, in TypeScript, which the build serves in a JavaScript project too; with `@zerolith/i18n` installed, `npx zerolith-i18n messages --out src/messages` regenerates the message modules.
## Rules
- Bind the signal, not its value: a live binding takes `count`, never `count.get()`, which freezes. Call `.get()` only inside `computed`, `effect`, `resource.params`, and event handlers.
- Components are capitalized functions written in JSX, one per file under `src/components/`, with their styles next to the markup, in a `css` literal or in a stylesheet beside the component imported with `?scoped`. JSX is a spelling of `html` templates: `class`, not `className`; `on:click` for events, `prop:value` for properties, `bool:disabled` for boolean attributes; lists and conditions are `each` and `when`.
- State that must survive an edit is a named signal: `signal(0, { name: "count" })`.
- `src/api.ts` and `src/messages/` are generated when they exist; change their inputs and regenerate, never edit them.
- A new page is a route in `src/routes.ts` and a component; shared state goes in `src/store.ts`, created with the first piece of it.
- Read `router` inside functions only: `src/router.ts` imports the route table, which imports the components, so a component module that uses `router` at its top level runs before the router exists.
- Files served as they are, such as data, icons, and `robots.txt`, go in `public/`.
- Only `.env` variables named `PUBLIC_*` reach the browser, as `import.meta.env.PUBLIC_*`.
## Where things go
| Change | Location |
| --- | --- |
| A page | `src/routes.ts` and `src/components/<page>.tsx` |
| Shared state or a request | `src/store.ts` |
| A message, once the project uses `@zerolith/i18n` | `messages/<locale>.json`, then regenerate |
| A style token | `src/styles.css`; component styles stay with the component |
| A static file | `public/` |Other choices change the lines that name the language and the tools. The framework's rules live in the zerolith skill rather than in this file, so the file stays about the project; the coding agents guide covers what an agent reads and when.
Scaffolding
The command line comes from npm as @zerolith/cli, so npx @zerolith/cli@alpha new my-app needs nothing installed first; see Command line.
| Command | Writes |
|---|---|
zerolith new my-app |
Asks each question, with TSX and the command line as the defaults |
zerolith new my-app --yes |
Takes every default without asking |
zerolith new my-app --template js |
JavaScript with html templates and JSDoc types |
zerolith new my-app --vite |
A project that develops and builds on Vite with @zerolith/vite |
zerolith new my-app --buildless |
A project that deploys its modules behind an import map instead of a bundle |
zerolith new my-app --example fullstack |
The fullstack example: Harbor, a team's projects and tasks under /app/, with its own Node backend |
Every starter pins the tool versions this repository tests with and installs its dependencies unless given --no-install. cd my-app && npm run dev starts it.