End-to-End Typsicherheit in Next.js mit tRPC erreichen
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Einführung
In der modernen Webentwicklung erfordert der Aufbau robuster und wartbarer Anwendungen eine nahtlose Kommunikation zwischen Frontend und Backend. Eine der anhaltenden Herausforderungen, denen sich Entwickler stellen, insbesondere bei der Arbeit mit TypeScript, besteht darin, die Typsicherheit über diese Grenze hinweg zu gewährleisten. Traditionell umfasste dies manuelle Typdefinitionen, Codegenerierungstools oder komplexe GraphQL-Setups, die jeweils ihren eigenen Overhead und ihre eigene Komplexität mit sich brachten. Diese Methoden führen oft zu einer Trennung, die zusätzliche Schritte erfordert, um Frontend- und Backend-Typen synchron zu halten, was zu potenziellen Fehlern und einer suboptimalen Entwicklererfahrung führt. Dieser Artikel befasst sich damit, wie tRPC, ein revolutionäres Framework, dieses Problem direkt angeht und eine elegante Lösung für die Erzielung von End-to-End-Typsicherheit zwischen Ihrer Next.js-Anwendung und Ihrem Node.js-Backend bietet, ohne dass eine umständliche Codegenerierung erforderlich ist.
Typsicherheit mit tRPC freischalten
Um die Leistungsfähigkeit von tRPC vollständig zu verstehen, lassen Sie uns zunächst einige grundlegende Konzepte klären.
Schlüsselbegriffe
- RPC (Remote Procedure Call): Ein Protokoll, das es einem Programm ermöglicht, einen Dienst von einem Programm auf einem anderen Computer in einem Netzwerk anzufordern, ohne die Netzwerkdetails verstehen zu müssen. Einfacher ausgedrückt geht es darum, eine Funktion aufzurufen, die sich woanders befindet, als wäre sie eine lokale Funktion.
- End-to-End-Typsicherheit: Die Garantie, dass die in Ihrem Backend definierten Typen (z. B. API-Schemata, Funktionssignaturen) auf Ihrem Frontend automatisch abgeleitet und erzwungen werden, wodurch Typenkonflikte und damit verbundene Fehler zur Kompilierzeit eliminiert werden, was zu zuverlässigeren Anwendungen führt.
- Zero-Generation: Ein Schlüsselmerkmal von tRPC, das bedeutet, dass es Typsicherheit erreicht, ohne irgendeinen Zwischenclientseitigen Code zu generieren. Stattdessen nutzt es die leistungsstarken Inferenzfähigkeiten von TypeScript direkt aus Ihrem Backend-Code.
Das tRPC-Prinzip
tRPC basiert auf einem einfachen, aber tiefgreifenden Prinzip: Nutzen Sie die Inferenz-Engine von TypeScript, um Typen direkt von Ihren Backend-Funktionsdefinitionen an Ihr Frontend weiterzugeben. Wenn Sie auf Ihrem Node.js-Backend mit TypeScript einen API-Endpunkt (in tRPC-Begriffen eine "Prozedur") definieren, leitet tRPC seine Eingabeargumente, den Rückgabetyp und potenzielle Fehler ab. Diese Typinformationen werden dann Ihrem Next.js-Frontend zur Verfügung gestellt, sodass Sie diese API-Endpunkte mit voller Typsicherheit aufrufen können, genau wie bei einer lokalen Funktion.
Implementierung von tRPC
Lassen Sie uns ein praktisches Beispiel durchgehen, um zu veranschaulichen, wie tRPC funktioniert.
Backend-Setup (Node.js)
Installieren Sie zunächst tRPC und Zod (das üblicherweise zur Schemavalidierung verwendet wird):
npm install @trpc/server zod
Definieren Sie nun Ihren tRPC-Router und Ihre Prozeduren.
// src/server/trpc.ts import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); // tRPC initialisieren export const router = t.router; export const publicProcedure = t.procedure; // src/server/routers/_app.ts import { publicProcedure, router } from './trpc'; import { z } from 'zod'; const appRouter = router({ // Eine 'query'-Prozedur zum Abrufen von Daten getUser: publicProcedure .input(z.object({ id: z.string().uuid() })) // Eingabeschema mit Zod definieren .query(async ({ input }) => { // In einer echten App würden Sie aus einer Datenbank abfragen console.log(`Benutzer mit ID abrufen: ${input.id}`); return { id: input.id, name: 'John Doe', email: 'john@example.com' }; }), // Eine 'mutation'-Prozedur zum Senden von Daten/Seiteneffekten createUser: publicProcedure .input(z.object({ name: z.string(), email: z.string().email() })) .mutation(async ({ input }) => { // In einer echten App würden Sie in einer Datenbank speichern console.log('Benutzer erstellen:', input); return { id: 'some-generated-uuid', ...input }; }), }); export type AppRouter = typeof appRouter; // Den Routertyp exportieren
Als Nächstes richten Sie Ihren API-Endpunkt in Next.js ein, um tRPC-Anfragen zu verarbeiten. Dies geschieht normalerweise in pages/api/trpc/[trpc].ts
oder direkt innerhalb eines Next.js App Router Route-Handlers.
// pages/api/trpc/[trpc].ts (für Pages Router) import { createNextApiHandler } from '@trpc/server/adapters/next'; import { appRouter } from '../../../server/routers/_app'; export default createNextApiHandler({ router: appRouter, createContext: () => ({ /* Kontext für Ihre Prozeduren, z. B. Datenbankverbindung */ }), });
Frontend-Setup (Next.js)
Installieren Sie tRPC-Client-Bibliotheken und React Query (üblicherweise für Datenabrufe verwendet):
npm install @trpc/client @tanstack/react-query @trpc/react-query
Erstellen Sie nun Ihren tRPC-Client und Provider.
// src/utils/trpc.ts import { httpBatchLink } from '@trpc/client'; import { createTRPCReact } from '@trpc/react-query'; import type { AppRouter } from '../server/routers/_app'; // Den Backend-Routertyp importieren export const trpc = createTRPCReact<AppRouter>(); // Typen direkt ableiten! // src/pages/_app.tsx (oder layout.tsx im App Router) import { useState } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { trpc } from '../utils/trpc'; import type { AppProps } from 'next/app'; function MyApp({ Component, pageProps }: AppProps) { const [queryClient] = useState(() => new QueryClient()); const [trpcClient] = useState(() => trpc.createClient({ links: [ httpBatchLink({ url: '/api/trpc', // Ihr tRPC API-Endpunkt }), ], }) ); return ( <trpc.Provider client={trpcClient} queryClient={queryClient}> <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> </trpc.Provider> ); } export default MyApp;
API-Prozeduren im Frontend aufrufen
Jetzt können Sie in jeder React-Komponente tRPC-Hooks mit voller Typsicherheit verwenden.
// src/components/UserDisplay.tsx import { trpc } from '../utils/trpc'; import React from 'react'; function UserDisplay() { // Verwenden Sie den useQuery-Hook, Typen werden aus AppRouter abgeleitet! const { data: user, isLoading, error } = trpc.getUser.useQuery({ id: 'a1b2c3d4-e5f6-7890-1234-567890abcdef' }); if (isLoading) return <div>Benutzer wird geladen...</div>; if (error) return <div>Fehler: {error.message}</div>; return ( <div> <h2>Benutzerdetails</h2> <p>ID: {user?.id}</p> <p>Name: {user?.name}</p> <p>E-Mail: {user?.email}</p> </div> ); } function CreateUserForm() { const createUserMutation = trpc.createUser.useMutation({ onSuccess: (data) => { alert(`Benutzer erstellt: ${data.name}`); // Bei Bedarf Abfragen ungültig machen oder Daten erneut abrufen }, onError: (err) => { alert(`Fehler beim Erstellen des Benutzers: ${err.message}`); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const name = formData.get('name') as string; const email = formData.get('email') as string; createUserMutation.mutate({ name, email }); // Eingabeargumente sind typgeprüft! }; return ( <form onSubmit={handleSubmit}> <h3>Neuen Benutzer erstellen</h3> <input name="name" type="text" placeholder="Name" required /> <input name="email" type="email" placeholder="E-Mail" required /> <button type="submit" disabled={createUserMutation.isLoading}> {createUserMutation.isLoading ? 'Erstelle...' : 'Benutzer erstellen'} </button> </form> ); } export default function Home() { return ( <div> <UserDisplay /> <CreateUserForm /> </div> ) }
Beachten Sie, wie trpc.getUser.useQuery
automatisch weiß, dass sein input
ein Objekt mit einer id
vom Typ string
sein muss und dass seine data
der Form { id: string, name: string, email: string }
vom Backend entsprechen wird. Ebenso erwartet createUserMutation.mutate
name
und email
, wie in der Backend-Prozedur createUser
definiert. Wenn Sie versuchen, einen falschen Typ zu übergeben oder ein erforderliches Feld zu überspringen, wird TypeScript dies sofort zur Kompilierzeit melden, lange bevor der Code ausgeführt wird. Das ist die Magie der Zero-Generation-End-to-End-Typsicherheit!
Anwendungsfälle
tRPC eignet sich besonders gut für:
- Monolithische oder eng gekoppelte Full-Stack-Anwendungen: Wenn Frontend und Backend vom selben Team oder innerhalb desselben Repositories entwickelt werden und die direkte Weitergabe von Typen effizient ist.
- Interne Tools/Admin-Panels: Wenn schnelle Entwicklung und starke Typgarantien entscheidend sind, um interne Betriebsfehler zu vermeiden.
- Projekte, die die Entwicklererfahrung priorisieren: tRPC reduziert den Kontextwechsel und den Aufwand für die manuelle Typensynchronisation erheblich.
- Microservices mit gemeinsamen Typen: Obwohl tRPC bei Monolithen glänzt, kann es auch in einer Microservice-Architektur Vorteile bieten, wenn Dienste mit gemeinsamen Typdefinitionen entwickelt werden.
Fazit
tRPC erweist sich als eine äußerst effektive Lösung, um die Lücke bei der Typsicherheit zwischen Next.js- und Node.js-Backends zu schließen. Durch die intelligente Nutzung der Inferenzfähigkeiten von TypeScript entfällt die Notwendigkeit manueller Typdefinitionen oder komplexer Codegenerierung und bietet eine wirklich nahtlose und angenehme Entwicklererfahrung. Mit tRPC können Sie Full-Stack-Anwendungen mit Zuversicht erstellen und wissen, dass Ihr Frontend und Backend immer in perfekter Typenharmonie kommunizieren. Es ermöglicht Entwicklern, robuste, fehlerfreie Anwendungen schneller auszuliefern und festigt seine Position als leistungsstarkes Werkzeug im modernen Webentwicklungs-Ökosystem.