Typen und Validierungen mit Zod in einem Monorepo teilen
James Reed
Infrastructure Engineer · Leapcell

Frontend und Backend mit gemeinsamen Schemas verbinden
Moderne Full-Stack-Anwendungen beinhalten oft ein Frontend-Framework wie Next.js und ein Backend-Framework wie Fastify. Eine häufige Herausforderung bei dieser Konfiguration ist die Aufrechterhaltung der Konsistenz zwischen den Datenstrukturen und Validierungsregeln, die von beiden Teilen der Anwendung verwendet werden. Ohne einen einheitlichen Ansatz finden sich Entwickler häufig mit duplizierten Typdefinitionen und Validierungslogik wieder, was zu Inkonsistenzen, erhöhtem Wartungsaufwand und einem höheren Fehlerrisiko führt. Dieses Problem wird in einem Monorepo, in dem mehrere Pakete zusammenarbeiten, noch verstärkt, was eine gemeinsame Quelle der Wahrheit für Schnittstellen und Datenvalidierung unerlässlich macht.
Hier glänzen Werkzeuge wie Zod. Zod ist eine TypeScript-first Schema-Deklarations- und Validierungsbibliothek, die statische Typen ableiten kann, was sie zu einem idealen Kandidaten für die Definition gemeinsamer Datenverträge macht. Durch die Zentralisierung unserer Schema-Definitionen in einem Monorepo-Workspace können wir sicherstellen, dass sowohl unser Next.js-Frontend als auch unser Fastify-Backend auf demselben Datenverständnis basieren, von API-Request-Bodies bis hin zu Datenbankmodellen. In diesem Artikel werden wir uns mit der praktischen Implementierung von Zod in einem Monorepo befassen, um das nahtlose Teilen von Typen und Validierungen zu erreichen und die Entwicklererfahrung sowie die Robustheit der Anwendung erheblich zu verbessern.
Die Bausteine verstehen
Bevor wir uns mit der Implementierung befassen, lassen Sie uns einige Kernkonzepte klären, die für unsere Diskussion entscheidend sind:
- Monorepo: Ein Monorepo ist ein einziges Repository, das mehrere, verschiedene Projekte oder "Pakete" enthält, die oft miteinander verbunden sind. Tools wie Yarn Workspaces oder Lerna werden häufig verwendet, um Abhängigkeiten und Build-Prozesse innerhalb eines Monorepos zu verwalten. Diese Struktur erleichtert die Code-Freigabe und Konsistenz über verschiedene Teile eines Anwendungsökosystems hinweg.
- Next.js: Ein beliebtes React-Framework zum Erstellen serverseitig gerenderter, statischer und clientseitiger Webanwendungen. Es wird oft für das Frontend von Full-Stack-Anwendungen verwendet.
- Fastify: Ein hochperformantes und entwicklerfreundliches Webframework für Node.js, das häufig zum Erstellen von Backend-APIs verwendet wird.
- Zod: Eine TypeScript-first Schema-Deklarations- und Validierungsbibliothek. Sie ermöglicht es Ihnen, Schemas für verschiedene Datentypen (Objekte, Zeichenketten, Zahlen usw.) zu definieren und dann Daten gegen diese Schemas zu validieren. Ein Schlüsselmerkmal von Zod ist die Fähigkeit, TypeScript-Typen direkt aus den Schema-Definitionen abzuleiten, wodurch die manuelle Typdeklaration überflüssig wird.
Das Problem der Duplikation
Betrachten Sie eine einfache Anwendung, die es Benutzern ermöglicht, einen neuen Beitrag zu erstellen. Ohne gemeinsame Schemas könnten Sie die Post-Schnittstelle und ihre Validierungsregeln unabhängig im Frontend und Backend definieren:
Frontend (Next.js):
// pages/posts/new.tsx interface CreatePostRequest { title: string; content: string; tags?: string[]; } // Client-seitige Validierungslogik...
Backend (Fastify):
// src/routes/posts.ts interface CreatePostInput { title: string; content: string; tags?: string[]; } // Joi-, Yup- oder manuelle Validierungslogik...
Diese Duplikation ist fragil. Wenn Sie ein neues Feld wie authorId hinzufügen oder eine Validierungsregel ändern (z. B. title muss mindestens 5 Zeichen lang sein), müssen Sie daran denken, beides zu aktualisieren. Dies ist eine häufige Fehlerquelle.
Die Monorepo- und Zod-Lösung
Unser Ansatz beinhaltet die Erstellung eines gemeinsamen Pakets innerhalb unseres Monorepos, das der Definition von Zod-Schemas gewidmet ist. Sowohl das Next.js-Frontend als auch das Fastify-Backend werden auf dieses gemeinsame Paket angewiesen sein und sicherstellen, dass sie exakt dieselben Datenverträge verwenden.
Lassen Sie uns eine hypothetische Monorepo-Struktur einrichten:
my-monorepo/
├── packages/
│ ├── api/ # Fastify-Backend
│ ├── web/ # Next.js-Frontend
│ └── common/ # Gemeinsame Zod-Schemas und Typen
├── package.json
├── yarn.lock
└── tsconfig.json
1. Schemas im common-Paket definieren:
Installieren Sie zuerst Zod in Ihrem common-Paket: yarn add zod.
Erstellen Sie nun eine Datei (z. B. schemas.ts) in packages/common/src, um Ihre Zod-Schemas zu definieren:
// packages/common/src/schemas.ts import { z } from 'zod'; export const createPostSchema = z.object({ title: z.string().min(5, { message: "Title must be at least 5 characters long" }), content: z.string().min(10, { message: "Content must be at least 10 characters long" }), tags: z.array(z.string()).optional(), authorId: z.string().uuid("Invalid author ID format").optional(), // Beispiel für ein neues Feld }); // TypeScript-Typ direkt aus dem Schema ableiten export type CreatePostInput = z.infer<typeof createPostSchema>;
2. Schemas im Fastify-Backend verwenden:
Installieren Sie das @fastify/zod-Plugin für Fastify, das Zod für die Validierung von Request-Bodies, Query-Strings und Parametern integriert. Installieren Sie in Ihrem api-Paket Zod und @fastify/zod: yarn add zod @fastify/zod.
Verwenden Sie dann das Schema aus Ihrem common-Paket in Ihren Fastify-Routen:
// packages/api/src/routes/posts.ts import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { createPostSchema, CreatePostInput } from '@my-monorepo/common/schemas'; // Aus common-Paket importieren export default async function (fastify: FastifyInstance) { fastify.post( '/posts', { schema: { body: createPostSchema, // Zod-Schema für Validierung verwenden }, }, async (request: FastifyRequest<{ Body: CreatePostInput }>, reply: FastifyReply) => { const { title, content, tags, authorId } = request.body; // Der Typ von request.body ist jetzt dank `CreatePostInput` und der Schema-Validierung von Fastify automatisch abgeleitet, // sodass sichergestellt ist, dass die Daten hier valide sind. // Simulieren des Speicherns des Beitrags console.log('Eingegangener valider Beitrag:', { title, content, tags, authorId }); return reply.status(201).send({ message: 'Post created successfully', data: { title, content, tags, authorId } }); } ); }
Erklärung:
- Wir importieren
createPostSchemadirekt aus demcommon-Paket. @fastify/zodkümmert sich automatisch um die Validierung, wenncreatePostSchemaanschema.bodyübergeben wird. Wenn der eingehende Request-Body dem Schema nicht entspricht, antwortet Fastify automatisch mit einem 400 Bad Request-Fehler.- Der Typ
CreatePostInputstellt sicher, dass derrequest.bodyin unserem Handler korrekt typisiert ist und Laufzeit-Typfehler für gültige Daten beseitigt werden.
3. Schemas im Next.js-Frontend verwenden:
Installieren Sie in Ihrem web-Paket Zod: yarn add zod. Verwenden Sie dann das Schema aus dem common-Paket für clientseitige Formulare und API-Aufrufe.
// packages/web/pages/create-post.tsx import React, { useState } from 'react'; import { createPostSchema, CreatePostInput } from '@my-monorepo/common/schemas'; // Aus common-Paket importieren const CreatePostPage: React.FC = () => { const [formData, setFormData] = useState<CreatePostInput>({ title: '', content: '', tags: [], }); const [errors, setErrors] = useState<Record<string, string | undefined>>({}); const [loading, setLoading] = useState(false); const [message, setMessage] = useState<string | null>(null); const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); // Fehlermeldung für das Feld löschen, während der Benutzer tippt if (errors[name]) { setErrors((prev) => ({ ...prev, [name]: undefined })); } }; const handleCreatePost = async (e: React.FormEvent) => { e.preventDefault(); setMessage(null); setErrors({}); setLoading(true); // Client-seitige Validierung mit dem gemeinsamen Zod-Schema const validationResult = createPostSchema.safeParse(formData); if (!validationResult.success) { const fieldErrors: Record<string, string | undefined> = {}; validationResult.error.errors.forEach((err) => { if (err.path.length > 0) { fieldErrors[err.path[0]] = err.message; } }); setErrors(fieldErrors); setLoading(false); return; } try { const response = await fetch('http://localhost:3001/posts', { // Annahme: Fastify läuft auf 3001 method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(validationResult.data), // Validierte Daten senden }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Failed to create post'); } setMessage('Post created successfully!'); setFormData({ title: '', content: '', tags: [] }); // Formular leeren } catch (error: any) { setMessage(`Error: ${error.message}`); } finally { setLoading(false); } }; return ( <div> <h1>Create New Post</h1> <form onSubmit={handleCreatePost}> <div> <label htmlFor="title">Title:</label> <input type="text" id="title" name="title" value={formData.title} onChange={handleChange} /> {errors.title && <p style={{ color: 'red' }}>{errors.title}</p>} </div> <div> <label htmlFor="content">Content:</label> <textarea id="content" name="content" value={formData.content} onChange={handleChange} /> {errors.content && <p style={{ color: 'red' }}>{errors.content}</p>} </div> <div> <label htmlFor="tags">Tags (comma-separated):</label> <input type="text" id="tags" name="tags" value={formData.tags?.join(',') || ''} onChange={(e) => { const value = e.target.value.split(',').map(tag => tag.trim()).filter(Boolean); setFormData((prev) => ({ ...prev, tags: value })); if (errors.tags) { setErrors((prev) => ({ ...prev, tags: undefined })); } }} /> {errors.tags && <p style={{ color: 'red' }}>{errors.tags}</p>} </div> <button type="submit" disabled={loading}> {loading ? 'Submitting...' : 'Create Post'} </button> </form> {message && <p>{message}</p>} </div> ); }; export default CreatePostPage;
Erklärung:
- Wiederum werden
createPostSchemaundCreatePostInputaus demcommon-Paket importiert. - Der Typ
CreatePostInputbietet automatisch Typsicherheit für unserenformData-Status. - Die clientseitige Validierung wird mit
createPostSchema.safeParse(formData)durchgeführt. Dies stellt sicher, dass die an das Backend gesendeten Daten bereits dem definierten Schema entsprechen, was eine bessere Benutzererfahrung bietet, indem Fehler lokal ohne Server-Roundtrip abgefangen werden. - Die von Zod abgeleiteten Typen reduzieren die Wahrscheinlichkeit, dass fehlerhafte Daten an das Backend gesendet oder daraus empfangene Daten falsch interpretiert werden, drastisch.
Anwendungsnutzen
Durch die Zentralisierung von Typen und Validierungen in einem common-Paket mit Zod:
- Eine einzige Quelle der Wahrheit: Alle Datenverträge werden an einem Ort definiert, was Redundanz reduziert und Konsistenz gewährleistet.
- Typsicherheit überall: Zod's
z.inferliefert automatisch TypeScript-Typen für Frontend und Backend und verbessert die Autovervollständigung und Fehlerprüfung während der Entwicklung. - Reduzierte Duplikation: Die Validierungslogik muss nicht für jede Umgebung neu geschrieben werden.
- Verbesserte Entwicklererfahrung: Entwickler können erforderliche Felder, Typen und Validierungsregeln leicht einsehen, indem sie das gemeinsame Schema betrachten.
- Erhöhte Robustheit: Inkonsistente Datenstrukturen oder Validierungsregeln, eine häufige Fehlerquelle, werden praktisch eliminiert.
- Einfacheres Refactoring: Änderungen an Datenstrukturen erfordern nur die Aktualisierung des Schemas in
common, und TypeScript hilft dabei, betroffene Bereiche sowohl im Frontend als auch im Backend hervorzuheben. - Vorteil der clientseitigen Validierung: Das Frontend kann Validierungen gegen dieselben Regeln wie das Backend durchführen, was den Benutzern sofortiges Feedback gibt und unnötige Netzwerkanfragen für ungültige Daten reduziert.
Fazit
Das Teilen von Typen und Validierungen über ein Monorepo hinweg ist eine Best Practice, die die Entwicklung und Wartung von Full-Stack-Anwendungen erheblich verbessert. Durch die Nutzung von Zod in einem dedizierten common-Paket etablieren wir eine robuste, einzige Quelle der Wahrheit für unsere Datenverträge und bieten wertvolle Typsicherheit und konsistente Validierungslogik von Next.js-Frontend-Formularen bis hin zu Fastify-Backend-API-Endpunkten. Dieser Ansatz optimiert nicht nur die Entwicklungs-Workflows, sondern führt auch zu zuverlässigerer und wartbarerer Software, was letztendlich eine stärkere Brücke zwischen den Schichten Ihrer Anwendung baut.

