Type Flow
This page explains the core design advantage in orva: types do not stop at one API boundary.
The chain
In a typical orva route, the useful chain looks like this:
app.use()adds shared variables or validated datavalidator()orzodValidator()parses request input- the route handler reads typed values from
c c.json()defines the response shapecreateRPC<typeof app>()reads request and response typescreateOpenAPIDocument()reads the same metadata for docs
Request
Every request enters through the Fetch API shape, and adapters only bridge host-specific request objects into that model.
Validator
JSON, form, query, param, header, cookie, and text inputs can all flow through one type-safe validation path.
Route
Context, middleware, and route composition stay readable, so business logic can stay concentrated inside handlers.
Contracts
Validation metadata can keep flowing into RPC and OpenAPI, reducing drift between runtime code, docs, and clients.
A complete example
import { z } from 'zod';
import { createOrva, defineMiddleware } from 'orvajs';
import { createRPC } from 'orvajs/rpc';
import { zodValidator } from 'orvajs/validator/zod';
const session = defineMiddleware<{ session: { role: string } }>(async (c, next) => {
c.set('session', { role: 'admin' });
await next();
});
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const app = createOrva()
.use(session)
.post(
'/users/:id',
zodValidator('json', createUserSchema),
(c) => {
const body = c.valid('json');
const role = c.get('session')?.role;
return c.json({
id: c.params.id,
role,
user: body,
}, 201);
},
);
const rpc = createRPC<typeof app>({
baseURL: 'https://api.example.com',
});import { z } from "zod";
import { createOrva, defineMiddleware } from "orvajs";
import { createRPC } from "orvajs/rpc";
import { zodValidator } from "orvajs/validator/zod";
const session = defineMiddleware(async (c, next) => {
c.set("session", { role: "admin" });
await next();
});
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email()
});
const app = createOrva().use(session).post(
"/users/:id",
zodValidator("json", createUserSchema),
(c) => {
const body = c.valid("json");
const role = c.get("session")?.role;
return c.json({
id: c.params.id,
role,
user: body
}, 201);
}
);
const rpc = createRPC({
baseURL: "https://api.example.com"
});What moves through the chain
app.use()
defineMiddleware() can push typed variables into downstream routes.
That makes c.get('session') typed instead of loosely attached state.
Validator output
zodValidator('json', schema) turns request body parsing into a typed handler input.
That makes c.valid('json') a contract boundary, not just a runtime helper.
Route response
When you return c.json(...), the response body type is preserved.
That becomes visible to RPC response inference even without extra OpenAPI response declarations.
RPC request and response
For createRPC<typeof app>():
bodyis inferred from validator outputparamis inferred from the route pathjson()andvalue()are inferred fromc.json(...)or route metadata
OpenAPI output
When you add route metadata or schema-aware validators, the same route can produce OpenAPI definitions without rewriting the request and response contract elsewhere.
Why this matters
Without this chain, teams usually describe the same thing in four places:
- runtime validation
- handler types
- client types
- API docs
orva tries to keep those layers close enough that they drift less often.