Erreichen von durchgängiger Typsicherheit in Full-Stack TypeScript mit tRPC
Grace Collins
Solutions Engineer · Leapcell

Einleitung
In der heutigen schnelllebigen Entwicklungsumgebung ist der Aufbau robuster und wartbarer Anwendungen von größter Bedeutung. Da sich die JavaScript-Ökosysteme weiterentwickeln, hat sich TypeScript als Eckpfeiler für die Verbesserung der Codequalität und des Entwicklererlebnisses erwiesen, insbesondere in größeren Projekten. Eine anhaltende Herausforderung bei Full-Stack-TypeScript-Anwendungen war jedoch die Aufrechterhaltung einer strengen Typsicherheit über den gesamten Stack hinweg. Typischerweise greifen Entwickler auf die manuelle Erstellung und Synchronisierung von API-Typen zwischen Backend und Frontend zurück, ein Prozess, der fehleranfällig ist, mühsam zu warten ist und eine erhebliche Reibungsquelle darstellt. Dies führt oft zu Laufzeit-Typ-Inkonsistenzen, die erst spät im Entwicklungszyklus oder noch schlimmer im produktiven Einsatz entdeckt werden.
Stellen Sie sich eine Welt vor, in der Ihr Frontend magisch die genauen Datentypen kennt, die Ihre Backend-API erwartet und zurückgibt, ohne zusätzliche Konfiguration oder doppelte Typdefinitionen. Genau das will tRPC erreichen. Durch einen neuartigen Ansatz für die API-Kommunikation ermöglicht tRPC den Entwicklern, echte durchgängige Typsicherheit zu erreichen, die Entwicklung zu straffen, Fehler zu reduzieren und das gesamte Entwicklererlebnis erheblich zu verbessern. Dieser Artikel wird untersuchen, wie tRPC diese bemerkenswerte Leistung erzielt, seine Kernkonzepte erläutern, seine Implementierung mit praktischen Codebeispielen demonstrieren und seine transformative Wirkung auf die Full-Stack-TypeScript-Entwicklung hervorheben.
Kernkonzepte von tRPC
Bevor wir uns mit den Implementierungsdetails befassen, sollten wir die grundlegenden Konzepte, die die Stärke von tRPC untermauern, klar verstehen.
-
tRPC (TypeScript Remote Procedure Call): Im Kern ist tRPC ein Framework, das es Ihnen ermöglicht, vollständig typsichere APIs zu erstellen, ohne Code-Generierung oder Schema-Definitionen zu benötigen. Es ermöglicht Ihrem Frontend, direkt Funktionen aufzurufen, die auf Ihrem Backend definiert sind, und sie so zu behandeln, als wären sie lokale Funktionen, während die Typintegrität gewahrt bleibt. Dies ist eine deutliche Abkehr von herkömmlichen REST- oder GraphQL-APIs, bei denen eine Zwischenschicht (wie ein Schema oder manuelle Typdefinitionen) erforderlich ist.
-
Prozeduren: In tRPC werden die Funktionalitäten Ihrer Backend-API als „Prozeduren“ bereitgestellt. Dies sind im Wesentlichen Funktionen, die auf Ihrem Server leben und von Ihrem Client aufgerufen werden können. Prozeduren können Abfragen (zum Abrufen von Daten), Mutationen (zum Ändern von Daten) oder Abonnements (für Echtzeitaktualisierungen) sein.
-
Router: Prozeduren sind in Routern organisiert. Ein Router ist eine Sammlung zusammengehöriger Prozeduren, die ein strukturiertes und modulares API-Design ermöglichen. Sie können Router verschachteln, um komplexere API-Hierarchien zu erstellen.
-
Inferenz aus Code: Die Magie von tRPC liegt in seiner Fähigkeit, Typen direkt aus Ihrem Backend-Code abzuleiten. Anstatt Sie zu zwingen, Typen separat zu definieren, nutzt tRPC die leistungsstarke Inferenz-Engine von TypeScript, um automatisch die notwendigen Typen für Ihren Client zu erstellen, basierend auf Ihren serverseitigen Prozedurdefinitionen. Dies eliminiert die Notwendigkeit manueller Typsynchronisierung.
-
Minimalismus: tRPC rühmt sich, leichtgewichtig und unvoreingenommen in Bezug auf Ihre Infrastruktur zu sein. Es schreibt Ihre Datenbank oder Ihr Frontend-Framework nicht vor und bietet Flexibilität für die Integration in bestehende Projekte.
Implementierung von durchgängiger Typsicherheit mit tRPC
Lassen Sie uns ein praktisches Beispiel durcharbeiten, um zu veranschaulichen, wie tRPC durchgängige Typsicherheit erreicht. Wir richten eine einfache Full-Stack-Anwendung mit einem tRPC-Backend und einem React-Frontend ein.
1. Einrichtung des Backends
Zuerst müssen wir ein Node.js-Projekt mit TypeScript initialisieren und die notwendigen tRPC-Pakete installieren.
mkdir trpc-example cd trpc-example npm init -y npm i express @trpc/server zod @trpc/client @trpc/react-query @tanstack/react-query npm i -D typescript ts-node @types/node @types/express npx tsc --init
Nun erstellen wir unseren Backend-Server. Wir definieren eine einfache Prozedur, die eine Liste von Benutzern abruft.
src/server/index.ts
import { inferAsyncReturnType, initTRPC } from '@trpc/server'; import * as trpcExpress from '@trpc/server/adapters/express'; import express, { Express } from 'express'; import { z } from 'zod'; // Zod für Schema-Validierung // Simuliere eine Datenbank const users = [ { id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }, ]; // tRPC initialisieren const t = initTRPC.create(); // Einen Router definieren const appRouter = t.router({ user: t.router({ getUsers: t.procedure.query(() => { return users; }), getUserById: t.procedure .input(z.object({ id: z.string() })) // Eingabeschema mit Zod definieren .query(({ input }) => { return users.find((user) => user.id === input.id); }), createUser: t.procedure .input(z.object({ name: z.string().min(3) })) // Eingabeschema für Mutationen definieren .mutation(({ input }) => { const newUser = { id: String(users.length + 1), name: input.name }; users.push(newUser); return newUser; }), }), }); // Typ-sicheren Router exportieren export type AppRouter = typeof appRouter; const app: Express = express(); const port = 3000; app.use( '/trpc', trpcExpress.createExpressMiddleware({ router: appRouter, createContext: ({ req, res }) => ({}), // Vorerst nur ein einfacher Kontext }) ); app.listen(port, () => { console.log(`Server listening on port ${port}`); });
Erklärung:
- Wir initialisieren tRPC mit
initTRPC.create()
. - Wir definieren einen
appRouter
, der einen verschachteltenuser
-Router enthält. getUsers
ist einequery
-Prozedur, die alle Benutzer zurückgibt. Beachten Sie, dass hier kein expliziter Rückgabetyp annotiert ist; tRPC leitet ihn ab.getUserById
ist eine weiterequery
, die eineid
als Eingabe nimmt. Wir verwendenzod
(z
), um ein Schema für die Eingabe zu definieren und so Typsicherheit und Laufzeitvalidierung zu gewährleisten.createUser
ist einemutation
-Prozedur, die zeigt, wie Daten erstellt, aktualisiert oder gelöscht werden. Sie verwendet ebenfalls Zod für die Eingabevalidierung.- Entscheidend ist, dass wir
AppRouter = typeof appRouter;
exportieren. Diese Zeile ist der Eckpfeiler der Typinferenz von tRPC. Wenn unser Frontend diesen Typ importiert, erhält es vollständige Kenntnisse über alle verfügbaren Prozeduren, ihre Eingaben und ihre Ausgaben.
2. Einrichtung des Frontends
Nun erstellen wir ein einfaches React-Frontend, das unsere tRPC-API verbraucht.
src/client/main.tsx
(Vite für eine schnelle React-Einrichtung)
import React from 'react'; import ReactDOM from 'react-dom/client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { httpBatchLink } from '@trpc/client'; import { trpc } from './trpc'; // Unsere tRPC-Client-Instanz import App from './App'; const queryClient = new QueryClient(); // Eine tRPC-Client-Instanz erstellen const trpcClient = trpc.createClient({ links: [ httpBatchLink({ url: 'http://localhost:3000/trpc', // URL unseres tRPC-Servers }), ], }); ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <trpc.Provider client={trpcClient} queryClient={queryClient}> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </trpc.Provider> </React.StrictMode> );
src/client/trpc.ts
(Diese Datei initialisiert unseren tRPC-Client)
import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/index'; // Importieren Sie den AppRouter-Typ vom Backend export const trpc = createTRPCReact<AppRouter>();
Erklärung:
- In
src/client/trpc.ts
importieren wirAppRouter
direkt von unserem Backend. Hier passiert die Magie!createTRPCReact<AppRouter>()
verwendet diesen importierten Typ, um eine vollständig typsicheretrpc
-Client-Instanz zu erstellen. - Der
trpc
-Client wird dann übertrpc.Provider
unserer React-Anwendung zur Verfügung gestellt.
src/client/App.tsx
import React, { useState } from 'react'; import { trpc } from './trpc'; function App() { const [newUserName, setNewUserName] = useState(''); const [userIdInput, setUserIdInput] = useState(''); // Verwenden Sie den typsicheren trpc-Client, um Backend-Prozeduren aufzurufen const { data: users, isLoading: isLoadingUsers, refetch: refetchUsers } = trpc.user.getUsers.useQuery(); const { data: userById, isLoading: isLoadingUserById } = trpc.user.getUserById.useQuery( { id: userIdInput }, { enabled: !!userIdInput } // Nur Abfrage ausführen, wenn userIdInput vorhanden ist ); const createUserMutation = trpc.user.createUser.useMutation({ onSuccess: () => { refetchUsers(); // Benutzer nach der Erstellung eines neuen Benutzers erneut abrufen setNewUserName(''); }, }); const handleCreateUser = () => { if (newUserName.trim()) { createUserMutation.mutate({ name: newUserName }); // Typsichere Eingabe! } }; return ( <div> <h1>tRPC Full-Stack Beispiel</h1> <section> <h2>Benutzer</h2> {isLoadingUsers && <p>Benutzer werden geladen...</p>} <ul> {users?.map((user) => ( <li key={user.id}> {user.id}: {user.name} </li> ))} </ul> <h3>Neuen Benutzer erstellen</h3> <input type="text" value={newUserName} onChange={(e) => setNewUserName(e.target.value)} placeholder="Name des neuen Benutzers" /> <button onClick={handleCreateUser} disabled={createUserMutation.isLoading}> {createUserMutation.isLoading ? 'Erstelle...' : 'Benutzer erstellen'} </button> {createUserMutation.isError && <p style={{ color: 'red' }}>Fehler beim Erstellen des Benutzers: {createUserMutation.error.message}</p>} </section> <section> <h2>Benutzer nach ID finden</h2> <input type="text" value={userIdInput} onChange={(e) => setUserIdInput(e.target.value)} placeholder="Benutzer-ID eingeben (z. B. 1)" /> {isLoadingUserById && <p>Benutzer wird geladen...</p>} {userById ? ( <p> Gefundener Benutzer: {userById.id}: {userById.name} </p> ) : ( userIdInput && !isLoadingUserById && <p>Benutzer nicht gefunden.</p> )} </section> </div> ); } export default App;
Durchgängige Typsicherheit in Aktion:
- Autovervollständigung für API-Aufrufe: Während Sie
trpc.user.
eingeben, schlägt Ihre IDEgetUsers
,getUserById
undcreateUser
vor – alles abgeleitet von Ihrem Backend-appRouter
. - Eingabevalidierung: Beim Aufruf von
trpc.user.getUserById.useQuery({ id: ... })
stellt TypeScript sicher, dass die Eigenschaftid
eine Zeichenkette ist, wie inz.string()
im Backend definiert. Wenn Sie versuchen,id: 123
(eine Zahl) zu übergeben, wird TypeScript dies sofort als Fehler markieren. Dasselbe gilt fürcreateUserMutation.mutate({ name: '...' })
. - Ausgabetypen: Die von
getUsers
zurückgegebenendata
(z.B.users
) sind automatisch alsArray<{ id: string; name: string; }>
typisiert. Diedata
vongetUserById
(z.B.userById
) ist als({ id: string; name: string; } | undefined)
typisiert. - Fehlerbehandlung: Das
createUserMutation.error
-Objekt wird ebenfalls typsicher sein und Einblicke in die Struktur von Fehlern geben, die vom tRPC-Server zurückgegeben werden.
Dieser nahtlose Typfluss von Backend zu Frontend, ohne manuelle Typduplizierung oder Schema-Generierungsschritte, ist das Kernversprechen von tRPC.
Anwendungsszenarien
tRPC glänzt in verschiedenen Anwendungsszenarien:
- Monorepos: Wenn Ihr Frontend und Backend im selben Repository liegen, ist das Teilen von Typen mit tRPC außergewöhnlich einfach und natürlich. Die enge Kopplung wird tatsächlich zu einer Stärke für die Entwicklererfahrung.
- Kleine bis mittlere Projekte: Für Projekte, bei denen der Aufwand für ein GraphQL-Schema oder die Generierung von REST-Swagger übermäßig erscheint, bietet tRPC eine leichtgewichtige und produktive Alternative.
- Interne Tools: Erstellung interner Dashboards oder Tools, bei denen schnelle Entwicklung und starke Typgarantien entscheidend sind.
- Prototyping: Schnelles Erstellen von vollständig typsicheren Prototypen, ohne sich um API-Verträge zwischen Teams kümmern zu müssen.
Während tRPC für viele Fälle wunderbar funktioniert, ist anzumerken, dass für stark entkoppelte Projekte mit mehreren Teams und unterschiedlichen Client-Technologien (z. B. mobile Apps, andere Programmiersprachen) traditionelle REST- oder GraphQL-APIs mit expliziten Schemas aufgrund ihrer erzwungenen, sprachunabhängigen Verträge möglicherweise immer noch eine geeignetere Wahl sind. Für die TypeScript-dominierte Full-Stack-Entwicklung bietet tRPC jedoch ein unvergleichliches Erlebnis.
Fazit
tRPC stellt einen bedeutenden Fortschritt in der Full-Stack-TypeScript-Entwicklung dar und verändert grundlegend, wie Entwickler die API-Erstellung und -Nutzung angehen. Durch die Nutzung des robusten Typinferenzsystems von TypeScript eliminiert tRPC den mühsamen und fehleranfälligen Prozess der manuellen Typsynchronisierung zwischen Backend und Frontend. Dies führt zu einem unvergleichlichen Entwicklererlebnis, das durch intelligente Autovervollständigung, Echtzeit-Typvalidierung und deutlich weniger Laufzeitfehler gekennzeichnet ist. Mit tRPC versteht Ihr Frontend Ihr Backend wirklich, was zu robusterer, wartbarerer und angenehmerer Full-Stack-Entwicklung führt. Es ermöglicht wirklich durchgängige Typsicherheit ohne Laufzeitaufwand für die Typgenerierung.