Back to articles
Next.js

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.

12 mins read
Next.jsTypeScriptReactWeb Development

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.

code
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

Project Structure
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

JSONtsconfig.json
{  "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:

Reactcomponents/ui/button.tsx
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:

Reactapp/dashboard/page.tsx
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

TypeScriptapp/api/users/route.ts
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 }    );  }}
Server Components

Render components on the server for better performance and SEO.

TypeScript Integration

Full type safety across your entire application stack.

API Routes

Built-in API functionality with TypeScript support.

Performance Optimization

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:

Reactcomponents/optimized-image.tsx
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.