Next.js TypeScript Best Practices – Type-Safe von Anfang an
Next.js und TypeScript sind die perfekte Kombination für moderne Webanwendungen. Während Next.js mit seinem hybriden Rendering-Ansatz überzeugt, sorgt TypeScript für Typsicherheit und weniger Runtime-Fehler. Doch nur mit den richtigen Next.js TypeScript Best Practices schöpfst du das volle Potenzial aus.
In diesem Artikel zeige ich dir bewährte Patterns für type-safe Next.js-Projekte – von der tsconfig über typed API Routes bis zu Runtime-Validierung.
Strikte TypeScript-Konfiguration
Der erste Schritt zu robusten Next.js TypeScript Best Practices ist eine strikte tsconfig.json. Next.js generiert eine Basis-Config, aber für Production-Apps empfehle ich schärfere Einstellungen:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
Besonders wichtig: noUncheckedIndexedAccess verhindert, dass du Array-Zugriffe ohne Null-Check machst. noImplicitReturns erzwingt explizite Returns in allen Code-Pfaden.
Typed Page Components und Props
Next.js 13+ nutzt das App Router Pattern. Für type-safe Pages definierst du Props mit generischen Types:
// app/blog/[slug]/page.tsx
interface PageProps {
params: {
slug: string;
};
searchParams: {
preview?: string;
};
}
export default async function BlogPost({ params, searchParams }: PageProps) {
const { slug } = params;
const isPreview = searchParams.preview === 'true';
// Type-safe data fetching
const post = await fetchPost(slug);
return (
<article>
<h1>{post.title}</h1>
{/* ... */}
</article>
);
}
Bei Server Components mit generateStaticParams typisierst du den Return-Wert:
export async function generateStaticParams(): Promise<{ slug: string }[]> {
const posts = await fetchAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
API Routes mit vollständiger Typsicherheit
Ein Kernelement der Next.js TypeScript Best Practices sind typed API Routes. Im App Router definierst du Route Handlers so:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface Post {
id: string;
title: string;
content: string;
published: boolean;
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get('limit') ?? '10', 10);
const posts: Post[] = await db.post.findMany({
take: limit,
where: { published: true }
});
return NextResponse.json<Post[]>(posts);
}
export async function POST(request: NextRequest) {
const body = await request.json();
// Validation wird später behandelt
const newPost: Post = await db.post.create({ data: body });
return NextResponse.json<Post>(newPost, { status: 201 });
}
Der Generic-Parameter bei NextResponse.json<T> gibt dir Autovervollständigung für Response-Payloads.
Runtime-Validierung mit Zod
TypeScript validiert nur zur Compile-Zeit. Für API-Input brauchst du Runtime-Validierung. Hier kommt Zod ins Spiel – ein Must-Have für Next.js TypeScript Best Practices:
npm install zod
Definiere Schemas und leite Types ab:
// lib/schemas.ts
import { z } from 'zod';
export const postSchema = z.object({
title: z.string().min(3).max(100),
content: z.string().min(10),
tags: z.array(z.string()).optional(),
published: z.boolean().default(false)
});
export type PostInput = z.infer<typeof postSchema>;
In API Routes validierst du dann:
// app/api/posts/route.ts
import { postSchema } from '@/lib/schemas';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validated = postSchema.parse(body); // Throws bei Fehlern
const newPost = await db.post.create({ data: validated });
return NextResponse.json(newPost, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
Type-Safe Data Fetching
Beim Fetchen von externen APIs definierst du Response-Types:
// lib/api.ts
interface GitHubRepo {
id: number;
name: string;
stargazers_count: number;
html_url: string;
}
export async function fetchRepos(username: string): Promise<GitHubRepo[]> {
const res = await fetch(`https://api.github.com/users/${username}/repos`, {
next: { revalidate: 3600 }
});
if (!res.ok) {
throw new Error(`GitHub API error: ${res.status}`);
}
const data: unknown = await res.json();
// Runtime-Validierung mit Zod
const repoArraySchema = z.array(z.object({
id: z.number(),
name: z.string(),
stargazers_count: z.number(),
html_url: z.string().url()
}));
return repoArraySchema.parse(data);
}
Dieses Pattern kombiniert TypeScript-Types mit Zod-Validierung – echte Next.js TypeScript Best Practices.
Shared Types zwischen Client und Server
Nutze einen zentralen Types-Ordner für wiederverwendbare Definitionen:
// types/index.ts
export interface User {
id: string;
email: string;
name: string | null;
role: 'admin' | 'user';
}
export interface ApiResponse<T> {
data: T | null;
error: string | null;
timestamp: string;
}
export type PaginatedResponse<T> = ApiResponse<T[]> & {
pagination: {
page: number;
perPage: number;
total: number;
};
};
Diese Types verwendest du sowohl in Server Components als auch in Client-Komponenten.
Typed Environment Variables
Für type-safe Env-Vars nutze ein Schema:
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'production', 'test'])
});
export const env = envSchema.parse(process.env);
Importiere env statt direkt process.env zu verwenden – so fängst du Config-Fehler beim Build.
Generic Helper für Type-Safe Forms
Bei Forms mit React Hook Form kombinierst du Zod-Schemas:
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { postSchema, type PostInput } from '@/lib/schemas';
export default function PostForm() {
const { register, handleSubmit, formState: { errors } } = useForm<PostInput>({
resolver: zodResolver(postSchema)
});
const onSubmit = async (data: PostInput) => {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
// ...
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('title')} />
{errors.title && <span>{errors.title.message}</span>}
{/* ... */}
</form>
);
}
Fazit
Diese Next.js TypeScript Best Practices machen deine Codebase robuster und wartbarer:
- Strikte tsconfig.json mit
noUncheckedIndexedAccess - Typed Props für Pages und Components
- Typed API Routes mit Generic Response-Types
- Runtime-Validierung mit Zod
- Type-safe Data Fetching mit Validierung
- Zentrale Type-Definitionen für Wiederverwendung
- Typed Environment Variables mit Zod-Schemas
TypeScript verhindert viele Bugs bereits zur Compile-Zeit. Kombiniert mit Zod für Runtime-Checks erhältst du maximale Sicherheit. Diese Patterns sind in jedem meiner Next.js-Projekte Standard – sie sparen langfristig Debugging-Zeit und verbessern Code-Qualität messbar.
Du arbeitest an einem Next.js-Projekt und möchtest TypeScript Best Practices von Anfang an richtig umsetzen? Ich helfe dir gerne bei Setup, Code-Reviews oder kompletten Implementierungen.