Przejdź do treści
Tworzenie Stron

Implementacja z Next.js 14 + TypeScript - Kompletny przewodnik

Poznaj najlepsze praktyki implementacji aplikacji w Next.js 14 z TypeScript. Przewodnik po App Router, konfiguracji TypeScript i optymalizacji wydajności.

26 stycznia 2025
15 min
Zagor Digital

Szybka odpowiedź

Next.js 14 z TypeScript to potężne połączenie oferujące pełne wsparcie dla typów, nowy App Router oraz szereg optymalizacji wydajności. Kluczowe elementy to: ścisła konfiguracja TypeScript, wykorzystanie Route Handlers dla logiki serwerowej, walidacja danych z bibliotekami jak Zod, oraz optymalizacja poprzez incremental compilation. Ta kombinacja zapewnia skalowalność, bezpieczeństwo typów i doskonałą developer experience.

Wprowadzenie

Implementacja nowoczesnych aplikacji webowych wymaga solidnych fundamentów. Next.js 14 wraz z TypeScript stanowi idealne rozwiązanie dla projektów wymagających wysokiej jakości kodu, skalowalności i wydajności. Nowy App Router wprowadzony w Next.js 13 i udoskonalony w wersji 14 zmienia sposób myślenia o strukturze aplikacji.

W tym przewodniku przedstawimy kompleksowe podejście do implementacji aplikacji Next.js 14 z TypeScript, od konfiguracji projektu po zaawansowane wzorce architektoniczne. Szczególny nacisk położymy na praktyki, które sprawdzą się w projektach produkcyjnych w latach 2024-2025.

Inicjalizacja projektu Next.js 14 z TypeScript

Tworzenie nowego projektu

Rozpocznij od utworzenia projektu z pełnym wsparciem TypeScript:

npx create-next-app@latest moja-aplikacja --typescript --tailwind --app

# Opcje konfiguracji:
# ✓ Would you like to use TypeScript? → Yes
# ✓ Would you like to use ESLint? → Yes
# ✓ Would you like to use Tailwind CSS? → Yes
# ✓ Would you like to use `src/` directory? → Yes
# ✓ Would you like to use App Router? → Yes
# ✓ Would you like to customize the default import alias? → Yes
# ✓ What import alias would you like configured? → @/*

Struktura projektu App Router

moja-aplikacja/
├── src/
│   ├── app/                    # App Router - strony i layouty
│   │   ├── layout.tsx          # Root layout
│   │   ├── page.tsx            # Strona główna
│   │   ├── api/                # API routes
│   │   │   └── hello/
│   │   │       └── route.ts    # Route handler
│   │   └── (routes)/           # Grupowanie tras
│   ├── components/             # Komponenty React
│   ├── lib/                    # Funkcje pomocnicze
│   └── types/                  # Definicje typów TypeScript
├── public/                     # Pliki statyczne
├── tsconfig.json               # Konfiguracja TypeScript
└── next.config.js              # Konfiguracja Next.js

Konfiguracja TypeScript dla produkcji

Optymalna konfiguracja tsconfig.json

{
  "compilerOptions": {
    // Ścisłe sprawdzanie typów
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    
    // Dodatkowe zabezpieczenia
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    
    // Konfiguracja modułów
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    
    // Next.js specific
    "jsx": "preserve",
    "incremental": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    
    // Path aliases
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/lib/*": ["./src/lib/*"],
      "@/types/*": ["./src/types/*"]
    },
    
    // Plugins
    "plugins": [
      {
        "name": "next"
      }
    ]
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Typowanie zmiennych środowiskowych

// src/types/env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: 'development' | 'production' | 'test'
      NEXT_PUBLIC_API_URL: string
      DATABASE_URL: string
      JWT_SECRET: string
      NEXT_PUBLIC_GA_ID?: string
    }
  }
}

export {}

Route Handlers z TypeScript

Podstawowy Route Handler

// src/app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'

// Schema walidacji
const createUserSchema = z.object({
  email: z.string().email('Nieprawidłowy adres email'),
  password: z.string().min(8, 'Hasło musi mieć minimum 8 znaków'),
  name: z.string().min(2, 'Imię musi mieć minimum 2 znaki')
})

// Typ dla odpowiedzi
type ApiResponse<T = any> = {
  success: boolean
  data?: T
  error?: string
  errors?: z.ZodError['errors']
}

export async function POST(request: NextRequest): Promise<NextResponse<ApiResponse>> {
  try {
    const body = await request.json()
    
    // Walidacja danych
    const validationResult = createUserSchema.safeParse(body)
    
    if (!validationResult.success) {
      return NextResponse.json({
        success: false,
        errors: validationResult.error.errors
      }, { status: 400 })
    }
    
    // Logika biznesowa
    const user = await createUser(validationResult.data)
    
    return NextResponse.json({
      success: true,
      data: user
    }, { status: 201 })
    
  } catch (error) {
    console.error('Error creating user:', error)
    return NextResponse.json({
      success: false,
      error: 'Wystąpił błąd podczas tworzenia użytkownika'
    }, { status: 500 })
  }
}

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const page = Number(searchParams.get('page')) || 1
  const limit = Number(searchParams.get('limit')) || 10
  
  try {
    const users = await getUsers({ page, limit })
    
    return NextResponse.json({
      success: true,
      data: users,
      meta: {
        page,
        limit,
        total: users.total
      }
    })
  } catch (error) {
    return NextResponse.json({
      success: false,
      error: 'Nie udało się pobrać użytkowników'
    }, { status: 500 })
  }
}

Middleware z TypeScript

// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifyJWT } from '@/lib/auth'

export async function middleware(request: NextRequest) {
  // Sprawdzanie autoryzacji dla chronionych tras
  if (request.nextUrl.pathname.startsWith('/admin')) {
    const token = request.cookies.get('auth-token')?.value
    
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
    
    try {
      const payload = await verifyJWT(token)
      
      // Dodanie informacji o użytkowniku do nagłówków
      const requestHeaders = new Headers(request.headers)
      requestHeaders.set('x-user-id', payload.userId)
      requestHeaders.set('x-user-role', payload.role)
      
      return NextResponse.next({
        request: {
          headers: requestHeaders,
        },
      })
    } catch (error) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: ['/admin/:path*', '/api/admin/:path*']
}

Server Components z TypeScript

Async Server Component

// src/app/products/page.tsx
import { Metadata } from 'next'
import { ProductCard } from '@/components/ProductCard'
import { getProducts } from '@/lib/api/products'
import { Product } from '@/types/product'

export const metadata: Metadata = {
  title: 'Produkty - Zagor Digital',
  description: 'Przeglądaj naszą ofertę produktów'
}

interface ProductsPageProps {
  searchParams: {
    category?: string
    sort?: 'price' | 'name' | 'date'
    page?: string
  }
}

export default async function ProductsPage({ searchParams }: ProductsPageProps) {
  const page = Number(searchParams.page) || 1
  const products = await getProducts({
    category: searchParams.category,
    sort: searchParams.sort || 'date',
    page,
    limit: 12
  })
  
  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Nasze produkty</h1>
      
      <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
        {products.items.map((product: Product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
      
      {products.totalPages > 1 && (
        <Pagination 
          currentPage={page} 
          totalPages={products.totalPages}
          baseUrl="/products"
          searchParams={searchParams}
        />
      )}
    </div>
  )
}

Error Boundary z TypeScript

// src/app/products/error.tsx
'use client'

import { useEffect } from 'react'
import { Button } from '@/components/ui/button'

interface ErrorProps {
  error: Error & { digest?: string }
  reset: () => void
}

export default function Error({ error, reset }: ErrorProps) {
  useEffect(() => {
    // Log błędu do systemu monitorowania
    console.error('Product page error:', error)
  }, [error])
  
  return (
    <div className="flex flex-col items-center justify-center min-h-[400px]">
      <h2 className="text-2xl font-bold mb-4">Coś poszło nie tak!</h2>
      <p className="text-muted-foreground mb-6">
        Nie udało się załadować produktów. Spróbuj ponownie.
      </p>
      <Button onClick={reset}>Spróbuj ponownie</Button>
    </div>
  )
}

Wzorce architektoniczne

Repository Pattern

// src/lib/repositories/userRepository.ts
import { User, CreateUserDTO, UpdateUserDTO } from '@/types/user'
import { db } from '@/lib/db'

export class UserRepository {
  async findById(id: string): Promise<User | null> {
    return await db.user.findUnique({
      where: { id }
    })
  }
  
  async findByEmail(email: string): Promise<User | null> {
    return await db.user.findUnique({
      where: { email }
    })
  }
  
  async create(data: CreateUserDTO): Promise<User> {
    return await db.user.create({
      data: {
        ...data,
        createdAt: new Date()
      }
    })
  }
  
  async update(id: string, data: UpdateUserDTO): Promise<User> {
    return await db.user.update({
      where: { id },
      data: {
        ...data,
        updatedAt: new Date()
      }
    })
  }
  
  async delete(id: string): Promise<void> {
    await db.user.delete({
      where: { id }
    })
  }
}

// Singleton instance
export const userRepository = new UserRepository()

Service Layer

// src/lib/services/userService.ts
import { userRepository } from '@/lib/repositories/userRepository'
import { CreateUserDTO, User } from '@/types/user'
import { hashPassword } from '@/lib/auth'
import { sendWelcomeEmail } from '@/lib/email'

export class UserService {
  async createUser(data: CreateUserDTO): Promise<User> {
    // Sprawdzenie czy użytkownik już istnieje
    const existingUser = await userRepository.findByEmail(data.email)
    if (existingUser) {
      throw new Error('Użytkownik z tym adresem email już istnieje')
    }
    
    // Hashowanie hasła
    const hashedPassword = await hashPassword(data.password)
    
    // Tworzenie użytkownika
    const user = await userRepository.create({
      ...data,
      password: hashedPassword
    })
    
    // Wysłanie emaila powitalnego
    await sendWelcomeEmail(user.email, user.name)
    
    return user
  }
  
  async getUserProfile(userId: string): Promise<Omit<User, 'password'>> {
    const user = await userRepository.findById(userId)
    
    if (!user) {
      throw new Error('Użytkownik nie znaleziony')
    }
    
    // Usunięcie hasła z odpowiedzi
    const { password, ...userWithoutPassword } = user
    return userWithoutPassword
  }
}

export const userService = new UserService()

Optymalizacja wydajności

Dynamic Imports z TypeScript

// src/components/DynamicChart.tsx
import dynamic from 'next/dynamic'
import { Skeleton } from '@/components/ui/skeleton'
import type { ChartProps } from '@/types/chart'

const Chart = dynamic<ChartProps>(
  () => import('@/components/Chart').then(mod => mod.Chart),
  {
    loading: () => <Skeleton className="h-[400px] w-full" />,
    ssr: false
  }
)

export function DynamicChart(props: ChartProps) {
  return <Chart {...props} />
}

Memoizacja i optymalizacja re-renderów

// src/components/ExpensiveList.tsx
import { memo, useMemo, useCallback } from 'react'

interface ListItem {
  id: string
  name: string
  value: number
}

interface ExpensiveListProps {
  items: ListItem[]
  onItemClick: (id: string) => void
}

export const ExpensiveList = memo<ExpensiveListProps>(({ items, onItemClick }) => {
  // Memoizacja obliczeń
  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => b.value - a.value)
  }, [items])
  
  // Memoizacja callbacków
  const handleClick = useCallback((id: string) => {
    onItemClick(id)
  }, [onItemClick])
  
  return (
    <ul className="space-y-2">
      {sortedItems.map(item => (
        <li 
          key={item.id}
          onClick={() => handleClick(item.id)}
          className="p-4 border rounded cursor-pointer hover:bg-gray-50"
        >
          <span className="font-medium">{item.name}</span>
          <span className="ml-auto">{item.value}</span>
        </li>
      ))}
    </ul>
  )
}, (prevProps, nextProps) => {
  // Custom porównanie props
  return (
    prevProps.items.length === nextProps.items.length &&
    prevProps.items.every((item, index) => item.id === nextProps.items[index].id)
  )
})

ExpensiveList.displayName = 'ExpensiveList'

Testowanie z TypeScript

Testy jednostkowe

// __tests__/services/userService.test.ts
import { userService } from '@/lib/services/userService'
import { userRepository } from '@/lib/repositories/userRepository'
import { hashPassword } from '@/lib/auth'

// Mock dependencies
jest.mock('@/lib/repositories/userRepository')
jest.mock('@/lib/auth')
jest.mock('@/lib/email')

describe('UserService', () => {
  beforeEach(() => {
    jest.clearAllMocks()
  })
  
  describe('createUser', () => {
    it('should create a new user successfully', async () => {
      const mockUser = {
        id: '1',
        email: 'test@example.com',
        name: 'Test User',
        password: 'hashed_password'
      }
      
      ;(userRepository.findByEmail as jest.Mock).mockResolvedValue(null)
      ;(hashPassword as jest.Mock).mockResolvedValue('hashed_password')
      ;(userRepository.create as jest.Mock).mockResolvedValue(mockUser)
      
      const result = await userService.createUser({
        email: 'test@example.com',
        name: 'Test User',
        password: 'password123'
      })
      
      expect(result).toEqual(mockUser)
      expect(userRepository.findByEmail).toHaveBeenCalledWith('test@example.com')
      expect(hashPassword).toHaveBeenCalledWith('password123')
    })
    
    it('should throw error if user already exists', async () => {
      ;(userRepository.findByEmail as jest.Mock).mockResolvedValue({ id: '1' })
      
      await expect(userService.createUser({
        email: 'existing@example.com',
        name: 'Test User',
        password: 'password123'
      })).rejects.toThrow('Użytkownik z tym adresem email już istnieje')
    })
  })
})

Testy integracyjne

// __tests__/api/users.test.ts
import { createMocks } from 'node-mocks-http'
import { POST } from '@/app/api/users/route'
import { NextRequest } from 'next/server'

describe('/api/users', () => {
  describe('POST', () => {
    it('should create user with valid data', async () => {
      const body = {
        email: 'test@example.com',
        password: 'password123',
        name: 'Test User'
      }
      
      const { req } = createMocks({
        method: 'POST',
        body
      })
      
      const request = new NextRequest('http://localhost:3000/api/users', {
        method: 'POST',
        body: JSON.stringify(body)
      })
      
      const response = await POST(request)
      const data = await response.json()
      
      expect(response.status).toBe(201)
      expect(data.success).toBe(true)
      expect(data.data).toHaveProperty('id')
    })
  })
})

Bezpieczeństwo i walidacja

Walidacja z Zod

// src/lib/validation/schemas.ts
import { z } from 'zod'

// Schema dla formularza kontaktowego
export const contactFormSchema = z.object({
  name: z.string()
    .min(2, 'Imię musi mieć minimum 2 znaki')
    .max(50, 'Imię może mieć maksymalnie 50 znaków'),
  
  email: z.string()
    .email('Nieprawidłowy adres email'),
  
  phone: z.string()
    .regex(/^\+?[0-9]{9,15}$/, 'Nieprawidłowy numer telefonu')
    .optional(),
  
  message: z.string()
    .min(10, 'Wiadomość musi mieć minimum 10 znaków')
    .max(1000, 'Wiadomość może mieć maksymalnie 1000 znaków'),
  
  consent: z.boolean()
    .refine(val => val === true, 'Musisz wyrazić zgodę')
})

// Typ TypeScript z schema
export type ContactFormData = z.infer<typeof contactFormSchema>

// Helper do walidacji
export async function validateData<T>(
  schema: z.ZodSchema<T>,
  data: unknown
): Promise<{ success: true; data: T } | { success: false; errors: z.ZodError }> {
  try {
    const validData = await schema.parseAsync(data)
    return { success: true, data: validData }
  } catch (error) {
    if (error instanceof z.ZodError) {
      return { success: false, errors: error }
    }
    throw error
  }
}

Sanityzacja danych

// src/lib/security/sanitize.ts
import DOMPurify from 'isomorphic-dompurify'

export function sanitizeHtml(dirty: string): string {
  return DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'target', 'rel']
  })
}

export function sanitizeInput(input: string): string {
  return input
    .trim()
    .replace(/[<>]/g, '') // Usunięcie podstawowych tagów HTML
    .slice(0, 1000) // Ograniczenie długości
}

// SQL Injection prevention z Prisma
export function createSafeQuery(searchTerm: string) {
  // Prisma automatycznie zabezpiecza przed SQL Injection
  return {
    where: {
      OR: [
        { name: { contains: searchTerm, mode: 'insensitive' } },
        { description: { contains: searchTerm, mode: 'insensitive' } }
      ]
    }
  }
}

Deployment i CI/CD

Konfiguracja dla Vercel

// next.config.js z TypeScript
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // Włączenie strict mode React
  reactStrictMode: true,
  
  // Konfiguracja obrazów
  images: {
    domains: ['cdn.zagordigital.pl'],
    formats: ['image/avif', 'image/webp'],
  },
  
  // Zmienne środowiskowe
  env: {
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL!,
  },
  
  // Optymalizacje produkcyjne
  swcMinify: true,
  
  // Konfiguracja dla TypeScript
  typescript: {
    // Nie zatrzymuj builda przy błędach TypeScript (nie zalecane dla produkcji)
    ignoreBuildErrors: false,
  },
  
  // Experimental features
  experimental: {
    typedRoutes: true, // Włącz typowane routing
  },
}

export default nextConfig

GitHub Actions CI

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Type check
        run: npm run type-check
      
      - name: Lint
        run: npm run lint
      
      - name: Test
        run: npm run test:ci
      
      - name: Build
        run: npm run build

Najczęściej zadawane pytania

Jak migrować z Pages Router do App Router?

Migracja wymaga przepisania komponentów stron na Server Components i przeniesienia API routes do struktury route.ts. Kluczowe zmiany to usunięcie getServerSideProps na rzecz bezpośredniego pobierania danych w komponentach.

Czy powinienem używać Server Components wszędzie?

Nie, używaj Server Components dla stron i komponentów nie wymagających interakcji. Client Components są niezbędne dla formularzy, stanów lokalnych i event handlerów.

Jak radzić sobie z typowaniem dynamicznych tras?

Next.js 14 wprowadza experimental typedRoutes które automatycznie generują typy dla tras. Alternatywnie możesz utworzyć własne typy dla params.

Jaka jest różnica między route.ts a page.tsx?

route.ts służy do tworzenia API endpoints (zastępuje pages/api), podczas gdy page.tsx renderuje strony HTML. Nie mogą współistnieć w tym samym folderze.

Jak optymalizować bundle size z TypeScript?

Używaj dynamic imports, tree shaking, oraz analizuj bundle za pomocą @next/bundle-analyzer. Upewnij się że tsconfig ma włączone isolatedModules.

Czy TypeScript spowalnia development?

Początkowa kompilacja może być wolniejsza, ale incremental compilation i cache znacząco przyspieszają kolejne buildy. Używaj npm run dev z hot reload.

Jak testować Server Components?

Testuj logikę biznesową osobno od komponentów. Dla testów integracyjnych używaj Playwright lub Cypress które renderują pełną aplikację.

Podsumowanie i kolejne kroki

Implementacja z Next.js 14 i TypeScript wymaga przemyślanego podejścia do architektury, ale zapewnia solidne fundamenty dla skalowalnych aplikacji. Kluczowe elementy sukcesu to ścisła konfiguracja TypeScript, wykorzystanie możliwości App Router oraz konsekwentne stosowanie wzorców architektonicznych.

Następne kroki:

  1. Skonfiguruj projekt z przedstawionymi ustawieniami TypeScript
  2. Zaimplementuj podstawowe Route Handlers z walidacją
  3. Stwórz reużywalne komponenty z właściwym typowaniem
  4. Dodaj testy jednostkowe i integracyjne
  5. Skonfiguruj CI/CD pipeline

Potrzebujesz wsparcia w implementacji aplikacji Next.js? Skontaktuj się z nami - zespół Zagor Digital pomoże w stworzeniu wydajnej i skalowalnej aplikacji.


Ostatnia aktualizacja: 26 stycznia 2025

Tworzenie Stron WWW

Zamow profesjonalna strone internetowa

Tagi:

Next.js 14
TypeScript
App Router
React
implementacja
web development

Udostępnij artykuł:

ZD

Zagor Digital

Eksperci w dziedzinie marketingu internetowego i pozycjonowania lokalnego.

Tworzenie Stron WWW

Zamow profesjonalna strone internetowa

Porady SEO co tydzien

Dolacz do newslettera i otrzymuj sprawdzone strategie pozycjonowania.