Alle Artikel
Next.js5 min

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:

  1. Strikte tsconfig.json mit noUncheckedIndexedAccess
  2. Typed Props für Pages und Components
  3. Typed API Routes mit Generic Response-Types
  4. Runtime-Validierung mit Zod
  5. Type-safe Data Fetching mit Validierung
  6. Zentrale Type-Definitionen für Wiederverwendung
  7. 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.

Next.js TypeScript Best PracticesNext.jsFreelancerWebentwicklungDüsseldorf