Skip to content
nooh
Esc
↑↓navigate↵open⌘Jpreview
On this page

Core concepts

The fundamental ideas behind Nooh — how the compiler works and how its pieces fit together.

Understanding these concepts will make the rest of the documentation clearer and help you reason about what Nooh does at each stage.

Filesystem routing

Every .ts file inside your routes directory is a route. The file’s path determines the URL; the file’s name suffix (.get.ts, .post.ts, …) determines the HTTP method.

File Route
routes/index.get.ts GET /
routes/health.get.ts GET /health
routes/users/index.post.ts POST /users
routes/users/[id].get.ts GET /users/:id
routes/files/$.get.ts GET /files/*

The compiler transforms this filesystem layout into a Hono router. You never write the routing logic yourself.

Generated router modules

For each unique route path (not method), Nooh generates a router module in .nooh/router/. That module exports one typed factory function per HTTP method:

// .nooh/router/users/[id].ts  (generated — do not edit)
export function get(handler) { ... }
export function post(handler) { ... }

Your route files import from these generated modules:

// src/routes/users/[id].get.ts
import { get } from "@router/users/[id]";

export default get((c) => c.json({ id: c.req.param("id") }));

The @router/* alias is configured by the template. The path after @router/ matches the route path, exactly.

The App type

Your nooh.config.ts can export an App interface that matches Hono’s Env type. Every generated router module imports this type and uses it as the App for all Context, Handler, and MiddlewareHandler generics — so your custom variables, bindings, and environment types flow through without any extra annotation.

// nooh.config.ts
export interface App {
  Variables: { user: User };
  Bindings: { DATABASE_URL: string };
}

export default config<App>({ routes: "src/routes" });

Dependency injection

Nooh ships a typed, compile-time dependency injection system. You declare services in containers, assign them scopes, and the compiler wires the right ones into each route.

There are four scopes:

singleton

Created once, the first time it is needed, and reused for every subsequent request. Good for things that are expensive to create and safe to share — database connections, HTTP clients, caches.

request

Created once per incoming request and shared within that request. Good for things that should be consistent within a request but isolated between them — authenticated user objects, per-request loggers.

transient

Created fresh every time it is resolved. Good for lightweight, stateless utilities.

value

A static value — not a factory. Equivalent to a singleton that wraps a constant. Good for configuration objects.

The compiler enforces scope rules at build time: a singleton cannot depend on a request-scoped service, because the singleton outlives the request. Violations are reported as errors before anything runs.

Validation

Nooh validates requests and responses using Standard Schema — a shared interface implemented by Zod, Valibot, ArkType, and others. You do not depend on a specific validation library; you depend on the interface.

Validation targets map to parts of the HTTP request:

Target Part of the request
json Request body (JSON)
form Request body (form data)
query URL query parameters
param URL path parameters
header Request headers
cookie Cookies
response Response body

The compiler generates the correct input/output types for each target and infers them into your handler’s context type — so c.req.valid("json") is typed without you writing a type annotation.

Error handling

Nooh supports typed error handlers at two levels: per-route and per-group. You declare error constructors alongside your handler, and the compiler generates an errors object that is injected into the handler input:

export default get({
  errors: {
    notFound: NotFoundError,
  },
  handler: ({ c, errors }) => {
    throw errors.notFound("Item not found.");
  },
});

You can also define a global onError handler in nooh.config.ts that catches anything not handled locally.

The compilation pipeline

nooh.config.ts + src/routes/ + src/deps/
       │
       ▼
   [discover]   Find all route files and dep containers
       │
       ▼
    [parse]     Extract method, path segments, group from each filename
       │
       ▼
   [analyze]    Build the dependency graph; validate scopes and cycles
       │
       ▼
  [introspect]  Import each route against a stub router to learn which deps it uses
       │
       ▼
  [generate]    Emit typed .nooh/ modules with only the needed deps wired in

Each stage produces diagnostics. Any error stops the pipeline and reports the problem with a file path and error code so you can fix it before deploying.

Was this page helpful?