Project structure
The fastest way to a sound layout is to scaffold one: pnpm create nwire my-app --template service gives you a single bounded context (app/ + config/ + batteries), and --template enterprise gives you a composed monorepo of contexts under apps/. The shapes below are what those produce — and what works once a project outgrows a single file.
There's no enforced layout — Nwire is happy with any TypeScript project; these are conventions, not rules.
Tiny: one folder
For a service with one bounded context, no actors, plain handlers:
my-app/
app/
main.ts # endpoint().use(httpKoa()).mount(app).run()
api.ts # wires array — { binding, handler } pairs
store.ts # in-memory or DB-backed store
middleware.ts # auth, logging (Koa.Middleware)
errors.ts # defineError() — typed throwables
package.json
tsconfig.jsonExample: examples/todo-app.
Medium: bounded contexts as folders
When the app has multiple bounded contexts (orders, billing, notifications) each one is self-contained under a folder. There's no special "module" concept — each domain folder is a collection of files that share imports. buildApp() walks the domains and pulls everything into one createApp:
my-app/
app/
main.ts # bootstrap — endpoint + adapters
app.ts # buildApp(): createApp + forgePlugins
api.ts # wires array (re-exports from domains)
domains/
orders/
events.ts # defineEvent — past-tense facts
actor.ts # defineActor — state + invariants
actions/place-order.ts # defineAction + handler
projections/orders-by-customer.ts # defineProjection + defineQuery
workflows/auto-charge.ts # defineWorkflow
routes/ # HTTP route bindings + handlers
billing/
events.ts
...
package.json
tsconfig.jsonEach domain folder is relocatable — copy orders/ to a sibling repo, fix imports, and you have a working module. That means no domain imports from app/main.ts, no domain imports between siblings via relative paths (use published events for cross-domain reactions).
buildApp() looks like:
// app/app.ts
import { createApp, defineRegistry } from "@nwire/app";
import { forgePlugins } from "@nwire/forge";
import { placeOrder } from "../domains/orders/actions/place-order";
import { autoCharge } from "../domains/orders/workflows/auto-charge";
import { ordersByCustomer, getOrdersByCustomer } from "../domains/orders/projections/orders-by-customer";
export function buildApp() {
return createApp({
appName: "my-app",
registry: defineRegistry({
handlers: [placeOrder, getOrdersByCustomer],
projections: [ordersByCustomer],
workflows: [autoCharge],
}),
plugins: [...forgePlugins()],
});
}Example: examples/moderation-queue.
Large: workspace packages + multi-app composition
Once a bounded context ships its own tests, configs, optional adapters, lift it into a workspace package. Each domain is its own App; the monolith composes them via appCompose:
my-repo/
packages/ # one pnpm workspace per domain
orders/ # @my-app/orders
package.json
src/
index.ts # exports buildOrdersApp()
events.ts
actor.ts
actions/
projections/
workflows/
routes/
__tests__/
billing/ # @my-app/billing
package.json
src/
index.ts # exports buildBillingApp()
...
apps/
monolith/ # deploy as one process
package.json
app/
main.ts # appCompose(orders, billing) + endpoint
orders-only/ # deploy domain split
package.json
app/
main.ts # buildOrdersApp + endpoint
package.json # pnpm workspace rootapps/monolith/app/main.ts:
import { appCompose } from "@nwire/app";
import { endpoint } from "@nwire/endpoint";
import { httpKoa } from "@nwire/koa";
import { buildOrdersApp } from "@my-app/orders";
import { buildBillingApp } from "@my-app/billing";
const orders = buildOrdersApp();
const billing = buildBillingApp();
const monolith = appCompose(orders, billing);
await orders.start();
await billing.start();
await endpoint("monolith", { port: 3000 })
.use(httpKoa({ prefix: "/api" }))
.mount(monolith)
.run();Each app keeps its own container. Adapters resolve handler context from the source app's container per dispatch — no cross-app coupling at the container layer.
Example: examples/station-management.
Naming
The conventions the framework's examples follow (not enforced, but they make codebases readable):
| Element | Convention | Example |
|---|---|---|
| Domain folder | plural noun | enrollments/ |
| Actor file | actor.ts (or <concept>.actor.ts) | order.actor.ts |
| Action file | <verb>-<noun>.ts | place-order.ts |
Action name | <plural>.<verb-noun> | orders.place-order |
| Workflow file | <verb>-<noun>.ts | auto-charge.ts |
| Projection file | <aggregate>.ts | orders-by-customer.ts |
| Query file | <verb>-<noun>.ts or co-located with projection | get-orders-by-customer.ts |
| Events file | events.ts | events.ts |
| Event symbol | <Subject>Was<VerbPast> | OrderWasPlaced |
Event name | <plural>.<thing-was-verbed> | orders.order-was-placed |
| Route file | <verb>-<noun>.ts | place-order.route.ts |
When to split a domain into its own workspace package
When two or more deployables (apps, services) need it, or when it ships its own test setup / adapters. Until then, the folder-under-domains/ layout is simpler and tests are easier.
Where to next
- HTTP routes — wiring routes onto an app.
- Modules + apps —
createApp+appCompose- the forge plugin in detail.
- Multi-transport — running the same wires as HTTP + queue worker + MCP under one endpoint.