Building a Modern Web App with Next.js 15 and TypeScript
A comprehensive guide to building production-ready web applications using Next.js 15 App Router, TypeScript, and modern development practices. Learn about Server Components, streaminsg, and performance optimization.
Building modern web applications requires a solid understanding of the latest tools and best practices. In this comprehensive guide, we'll walk through creating a production-ready web application using Next.js 15, TypeScript, and modern development practices.
Getting Started with Next.js 15
Next.js 15 introduces significant improvements to the App Router, React Server Components, and development experience. Let's start by setting up a new project with the latest features.
npx create-next-app@latest my-modern-app --typescript --tailwind --eslint --app
This command creates a new Next.js project with TypeScript, Tailwind CSS, ESLint, and the App Router configured by default.
Project Structure and Organization
my-modern-app/├── app/│ ├── globals.css│ ├── layout.tsx│ ├── page.tsx│ ├── api/│ │ └── users/│ │ └── route.ts│ └── dashboard/│ ├── layout.tsx│ └── page.tsx├── components/│ ├── ui/│ │ ├── button.tsx│ │ └── card.tsx│ └── shared/│ └── navigation.tsx├── lib/│ ├── utils.ts│ └── validations.ts└── types/ └── index.ts
Setting Up TypeScript Configurations
{ "compilerOptions": { "target": "ES2017", "lib": ["dom", "dom.iterable", "ES6"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "baseUrl": ".", "paths": { "@/*": ["./*"], "@/components/*": ["components/*"], "@/lib/*": ["lib/*"], "@/types/*": ["types/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"]}
Creating Reusable Components
Building a component library ensures consistency and reusability across your application. Here's how to create properly typed React components:
import { ButtonHTMLAttributes, forwardRef } from 'react';import { cn } from '@/lib/utils';interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { variant?: 'primary' | 'secondary' | 'outline' | 'ghost'; size?: 'sm' | 'md' | 'lg'; isLoading?: boolean;}const Button = forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant = 'primary', size = 'md', isLoading, children, ...props }, ref) => { const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:pointer-events-none'; const variants = { primary: 'bg-blue-600 text-white hover:bg-blue-700', secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300', outline: 'border border-gray-300 bg-transparent hover:bg-gray-50', ghost: 'hover:bg-gray-100' }; const sizes = { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4', lg: 'h-12 px-6 text-lg' }; return ( <button className={cn( baseStyles, variants[variant], sizes[size], className )} ref={ref} disabled={isLoading} {...props} > {isLoading ? ( <div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" /> ) : null} {children} </button> ); });Button.displayName = 'Button';export { Button };
Server Components and Data Fetching
Next.js 15 Server Components allow you to fetch data on the server, reducing client-side JavaScript and improving performance:
import { Suspense } from 'react';import { UserCard } from '@/components/user-card';import { LoadingSpinner } from '@/components/ui/loading-spinner';async function getUsers() { const res = await fetch('https://api.example.com/users', { next: { revalidate: 3600 } // Revalidate every hour }); if (!res.ok) { throw new Error('Failed to fetch users'); } return res.json();}async function UsersList() { const users = await getUsers(); return ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {users.map((user: User) => ( <UserCard key={user.id} user={user} /> ))} </div> );}export default function DashboardPage() { return ( <div className="container mx-auto py-8"> <h1 className="text-3xl font-bold mb-8">Dashboard</h1> <Suspense fallback={<LoadingSpinner />}> <UsersList /> </Suspense> </div> );}
API Routes with TypeScript
import { NextRequest, NextResponse } from 'next/server';import { z } from 'zod';const CreateUserSchema = z.object({ name: z.string().mins(2), email: z.string().email(), age: z.number().mins(18)});export async function GET() { try { // Fetch users from database const users = await db.user.findMany(); return NextResponse.json({ success: true, data: users }); } catch (error) { return NextResponse.json( { success: false, error: 'Failed to fetch users' }, { status: 500 } ); }}export async function POST(request: NextRequest) { try { const body = await request.json(); const validatedData = CreateUserSchema.parse(body); const user = await db.user.create({ data: validatedData }); return NextResponse.json({ success: true, data: user }, { status: 201 }); } catch (error) { if (error instanceof z.ZodError) { return NextResponse.json( { success: false, error: error.errors }, { status: 400 } ); } return NextResponse.json( { success: false, error: 'Internal server error' }, { status: 500 } ); }}
Render components on the server for better performance and SEO.
Full type safety across your entire application stack.
Built-in API functionality with TypeScript support.
Streaminsg, caching, and bundle optimization out of the box.
Performance Optimization
Next.js 15 provides several built-in optimizations, but there are additional steps you can take to ensure optimal performance:
import Image from 'next/image';interface OptimizedImageProps { src: string; alt: string; width: number; height: number; priority?: boolean;}export function OptimizedImage({ src, alt, width, height, priority = false }: OptimizedImageProps) { return ( <Image src={src} alt={alt} width={width} height={height} priority={priority} placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q==" className="rounded-lg shadow-md" /> );}
Deployment and Production Considerations
When deploying your Next.js application to production, consider these important factors:
- Environment variables and secrets management
- Database connection pooling and optimization
- CDN configuration for static assets
- Monitoring and error tracking setup
- Performance monitoring and analytics
- Security headers and HTTPS configuration
“A well-architected Next.js application should be fast, secure, and maintainable. Focus on developer experience without compromising on performance.”