Typsichere APIs ohne Codegenerierung
Hono RPC erzeugt einen vollständig typisierten API-Client direkt aus dem Rückgabetyp der Serverrouten, ohne separaten Codegenerierungsschritt und ohne dupliziertes Schema. Der Server ist gleichzeitig die einzige Quelle der Wahrheit für Endpunkt, Payload und Antwort.
Inhaltsverzeichnis
- 1. Hono als schlankes, Runtime-agnostisches Web-Framework
- 2. AppType: der Vertrag zwischen Server und Client
- 3. Der hc-Client: Typinferenz statt Codegenerierung
- 4. Request-Validierung mit Zod-Middleware
- 5. Routen verketten statt separat registrieren
- 6. Hono RPC im Vergleich zu tRPC
- 7. Deployment auf Cloudflare Workers ohne Anpassung
- 8. Typisierte Fehlerantworten im Client behandeln
- 9. Grenzen: Monorepo-Kopplung und Typ-Performance
- 10. Zusammenfassung
- 11. FAQ
1. Hono als schlankes, Runtime-agnostisches Web-Framework
Hono ist ein minimalistisches Web-Framework, das bewusst auf Web-Standard-APIs wie Request und Response aufbaut, statt eine eigene Node.js-spezifische Abstraktion zu erfinden. Dadurch läuft dieselbe Codebasis unverändert auf Cloudflare Workers, Deno, Bun, Node.js und weiteren Runtimes, ohne Adapter-Schicht in der Anwendungslogik.
Die Routendefinition folgt einem verketteten, Express-ähnlichen Stil, ist dabei aber von Grund auf mit TypeScript-Generics gebaut. Jede Methode wie get oder post gibt eine neue, erweiterte App-Instanz zurück, deren Typ alle bisher registrierten Routen kennt, was später die Grundlage für den typsicheren Client bildet.
Diese Bauweise unterscheidet Hono deutlich von größeren Frameworks, die häufig eine eigene Laufzeitumgebung mit fest verdrahteten Node.js-APIs voraussetzen. Weil Hono auf wenige Kernkonzepte setzt, bleibt die Lernkurve flach, während gleichzeitig genug Erweiterbarkeit über Middleware für reale Produktionsanforderungen erhalten bleibt. Gerade für kleinere Microservices oder API-Gateways zahlt sich diese Schlankheit direkt in kürzeren Kaltstartzeiten aus.
// src/index.ts
import { Hono } from 'hono';
const app = new Hono()
.get('/posts/:id', (c) => {
const id = c.req.param('id');
return c.json({ id, title: 'Beispielbeitrag' });
});
export default app;
export type AppType = typeof app;
2. AppType: der Vertrag zwischen Server und Client
Der entscheidende Baustein von Hono RPC ist die Zeile export type AppType = typeof app. Statt ein separates OpenAPI-Dokument oder eine GraphQL-Schemadatei zu pflegen, wird der TypeScript-Typ der App-Instanz selbst zum Vertrag. Jede Route, jeder Parameter und jeder Rückgabetyp aus c.json() ist darin bereits enthalten.
Damit dieser Typ im Client verfügbar ist, muss er lediglich importiert werden, üblicherweise über ein gemeinsames Package in einem Monorepo oder über einen relativen Import bei Full-Stack-Projekten mit gemeinsamem TypeScript-Setup. Der eigentliche JavaScript-Code der Serverimplementierung wird dabei nicht mitgeliefert, nur der Typ, sodass Serverlogik und Client-Bundle sauber getrennt bleiben.
3. Der hc-Client: Typinferenz statt Codegenerierung
Mit der Funktion hc erzeugt Hono einen Client, dessen komplette Struktur, verschachtelte Pfadsegmente, HTTP-Methoden und Parameter, aus dem importierten AppType abgeleitet wird. Ein Aufruf wie client.posts[':id'].$get({ param: { id: '42' } }) wird vom Compiler exakt gegen die Serverroute geprüft, inklusive der erwarteten Parameterstruktur.
Der Rückgabetyp der Anfrage entspricht exakt dem, was die Serverroute über c.json() zurückgegeben hat. Ändert sich auf dem Server ein Feldname oder wird ein Feld entfernt, meldet der Client-Code sofort einen Kompilierfehler an jeder Stelle, die dieses Feld verwendet, ganz ohne dass ein Codegenerierungsschritt zwischengeschaltet werden muss.
// client.ts
import { hc } from 'hono/client';
import type { AppType } from './index';
const client = hc<AppType>('https://api.mironsoft.de');
async function loadPost(id: string) {
const res = await client.posts[':id'].$get({ param: { id } });
if (!res.ok) {
throw new Error(`Fehler ${res.status}`);
}
const post = await res.json();
return post; // { id: string; title: string }
}
4. Request-Validierung mit Zod-Middleware
Für Anfragen mit Body, Query-Parametern oder Headern kombiniert man Hono typischerweise mit @hono/zod-validator. Die Middleware validiert eingehende Daten zur Laufzeit gegen ein Zod-Schema und macht das validierte, typisierte Ergebnis über c.req.valid('json') im Handler verfügbar, statt auf rohe, unvalidierte Werte zuzugreifen.
Der entscheidende Effekt für Hono RPC ist, dass der Typ des validierten Werts automatisch in den Routentyp einfließt. Der generierte Client kennt dadurch nicht nur die URL-Struktur, sondern auch die exakte, vom Zod-Schema abgeleitete Form des erwarteten Request-Bodys, sodass ein fehlerhafter Aufruf bereits beim Schreiben des Client-Codes auffällt.
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const CreatePostSchema = z.object({
title: z.string().min(3),
content: z.string(),
});
const app = new Hono().post(
'/posts',
zValidator('json', CreatePostSchema),
async (c) => {
const data = c.req.valid('json');
const post = await db.post.create({ data });
return c.json(post, 201);
},
);
export type AppType = typeof app;
5. Routen verketten statt separat registrieren
Für korrekte Typinferenz ist entscheidend, dass Routen tatsächlich verkettet werden, statt sie mit separaten Aufrufen wie app.get(...); app.post(...); zu registrieren. Nur die verkettete Form akkumuliert alle Routentypen in derselben App-Instanz, weil jede Methode eine neu typisierte Instanz zurückgibt, die die vorherige erweitert.
Bei größeren Anwendungen werden Teilrouten über app.route('/posts', postsRouter) eingebunden, wobei der Typ des Unterrouters ebenfalls exportiert und in die Elternroute eingemischt werden muss, damit der finale AppType wirklich sämtliche Endpunkte kennt.
// routes/posts.ts
import { Hono } from 'hono';
export const postsRouter = new Hono()
.get('/', async (c) => c.json(await db.post.findMany()))
.post('/', async (c) => c.json({ id: 'new-id' }, 201));
// index.ts
import { Hono } from 'hono';
import { postsRouter } from './routes/posts';
const app = new Hono().route('/posts', postsRouter);
export type AppType = typeof app;
6. Hono RPC im Vergleich zu tRPC
tRPC verfolgt ein ähnliches Ziel, typsichere APIs ohne Codegenerierung, arbeitet dabei aber mit einer eigenen Prozedur-Abstraktion aus Queries und Mutations statt regulärer HTTP-Routen. Hono RPC bleibt näher an klassischem REST, jede Route ist ein normaler HTTP-Endpunkt mit Pfad und Methode, der auch ohne den generierten Client, etwa von externen Systemen oder über curl, ganz normal ansprechbar bleibt.
Dieser Unterschied macht Hono RPC besonders attraktiv, wenn dieselbe API sowohl von einem eigenen typsicheren Frontend als auch von Drittsystemen konsumiert werden soll, während sich tRPC eher für in sich geschlossene Full-Stack-Anwendungen eignet, bei denen niemals ein fremder Client gegen die API sprechen muss.
7. Deployment auf Cloudflare Workers ohne Anpassung
Weil Hono ausschließlich Web-Standard-APIs verwendet, lässt sich dieselbe App-Definition ohne Änderung als Cloudflare Worker deployen. Der Export erfolgt über ein Objekt mit einer fetch-Methode, was exakt der Worker-Signatur entspricht, sodass kein zusätzlicher Adapter zwischen Hono und der Workers-Runtime nötig ist.
Für typisierte Zugriffe auf Cloudflare-spezifische Bindings wie KV-Namespaces oder D1-Datenbanken unterstützt Hono einen generischen Env-Typparameter, der beim Erzeugen der App-Instanz angegeben wird. Handler erhalten dadurch typisierten Zugriff auf c.env.MY_KV, ohne auf any zurückgreifen zu müssen.
type Bindings = {
MY_KV: KVNamespace;
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>().get('/cached/:key', async (c) => {
const value = await c.env.MY_KV.get(c.req.param('key'));
return c.json({ value });
});
export default app;
8. Typisierte Fehlerantworten im Client behandeln
Der von hc() erzeugte Client wirft bei einer HTTP-Fehlerantwort standardmäßig keine Exception, sondern liefert ein Response-ähnliches Objekt mit einem ok-Flag zurück. Der Aufrufer muss dieses Flag explizit prüfen, bevor er auf die Antwortdaten zugreift, was TypeScript über eine Typ-Verengung im Erfolgsfall unterstützt.
Definiert eine Route über c.json(data, 404) mehrere mögliche Statuscodes mit unterschiedlichen Rückgabetypen, kennt der Client-Typ diese Varianten ebenfalls und erlaubt es, anhand des Statuscodes zwischen unterschiedlich geformten Antwortkörpern zu unterscheiden, statt nur einen einzigen, generischen Fehlertyp anzunehmen.
const res = await client.posts[':id'].$get({ param: { id } });
if (res.status === 404) {
const notFound = await res.json(); // { error: string }
console.warn(notFound.error);
return null;
}
if (!res.ok) {
throw new Error(`Unerwarteter Status ${res.status}`);
}
return res.json();
9. Grenzen: Monorepo-Kopplung und Typ-Performance
Hono RPC funktioniert am reibungslosesten, wenn Server und Client denselben TypeScript-Compiler-Kontext teilen, üblicherweise innerhalb eines Monorepos mit Tools wie Turborepo oder Nx. Für vollständig getrennte Repositories muss der AppType über ein veröffentlichtes Package verteilt werden, was zusätzliche Build- und Versionierungsschritte erfordert.
Bei sehr großen Anwendungen mit hunderten verketteten Routen kann die Typinferenz spürbar langsamer werden, da der Compiler bei jeder Nutzung des Clients den gesamten akkumulierten Routentyp neu auflösen muss. In solchen Fällen hilft eine Aufteilung in mehrere kleinere Sub-Router mit jeweils eigenem, unabhängig exportiertem Typ. In der Praxis lohnt sich diese Aufteilung ohnehin schon aus Gründen der Wartbarkeit, sodass die spätere Typ-Performance selten der alleinige Auslöser für eine Umstrukturierung ist.
| Merkmal | Hono RPC | tRPC | REST mit manuell gepflegten Typen |
|---|---|---|---|
| Codegenerierung | Keine, reine Typinferenz | Keine, reine Typinferenz | Meist ja, etwa via OpenAPI-Generator |
| API-Stil | Klassisches REST über HTTP | Eigene Prozedur-Abstraktion | Klassisches REST |
| Fremdzugriff ohne Client | Uneingeschränkt möglich | Erschwert, prozedurgebunden | Uneingeschränkt möglich |
| Edge-Tauglichkeit | Nativ, Web-Standard-APIs | Abhängig vom Adapter | Abhängig vom Framework |
| Kopplung Server/Client | Gemeinsamer TypeScript-Kontext nötig | Gemeinsamer TypeScript-Kontext nötig | Entkoppelt über Schema-Datei |
Mironsoft
TypeScript-Migration, Typsicherheit und Team-Onboarding
JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?
Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.
Migrations-Fahrplan
Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.
Strict-Mode-Einführung
tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.
Team-Onboarding
Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.
10. Zusammenfassung
TypeScript mit Hono RPC
Vertrag
AppType leitet den Client-Typ direkt aus der Serverroute ab
Client-Erzeugung
hc
Validierung
zValidator mit Zod fließt automatisch in den Routentyp ein
Runtime
Web-Standard-APIs laufen nativ auf Cloudflare Workers und mehr