Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand Next.js fundamentals with TypeScript ๐ฏ
- Build full-stack React apps with type safety ๐๏ธ
- Debug common Next.js TypeScript issues ๐
- Write type-safe Next.js applications โจ
๐ฏ Introduction
Welcome to the exciting world of Next.js with TypeScript! ๐ In this comprehensive guide, weโll explore how to build modern, full-stack React applications with the power of Next.js and the safety of TypeScript.
Youโll discover how Next.js transforms your React development experience with server-side rendering, static generation, and powerful TypeScript integration. Whether youโre building e-commerce sites ๐, blogs ๐, or complex web applications ๐, understanding Next.js with TypeScript is essential for modern web development.
By the end of this tutorial, youโll feel confident building production-ready Next.js applications with complete type safety! Letโs dive in! ๐
๐ Understanding Next.js with TypeScript
๐ค What is Next.js?
Next.js is like a Swiss Army knife for React development ๐ ๏ธ. Think of it as a framework that gives your React apps superpowers - server-side rendering, static generation, automatic code splitting, and built-in TypeScript support!
In TypeScript terms, Next.js provides a complete development ecosystem with built-in type definitions and seamless TypeScript integration. This means you can:
- โจ Get automatic TypeScript configuration
- ๐ Enjoy built-in performance optimizations
- ๐ก๏ธ Benefit from full-stack type safety
- ๐ฆ Use file-based routing with TypeScript
๐ก Why Use Next.js with TypeScript?
Hereโs why developers love this powerful combination:
- Zero Config TypeScript ๐ง: Next.js handles TypeScript setup automatically
- Full-Stack Type Safety ๐: From API routes to frontend components
- Amazing Developer Experience ๐ป: Hot reloading, error handling, and IntelliSense
- Production Ready ๐: Built-in optimizations and deployment features
- Huge Ecosystem ๐: Incredible community and plugin support
Real-world example: Imagine building an online store ๐. With Next.js and TypeScript, you can have type-safe product pages, API routes for inventory, and lightning-fast performance!
๐ง Basic Setup and Configuration
๐ Creating a New Next.js TypeScript Project
Letโs start with a friendly setup:
# ๐ Create a new Next.js app with TypeScript
npx create-next-app@latest my-awesome-app --typescript --tailwind --eslint --app
# ๐ Navigate to your project
cd my-awesome-app
# ๐ฎ Start the development server
npm run dev
๐ก Explanation: The --typescript
flag automatically sets up TypeScript configuration, while --app
gives us the new app directory structure!
๐ฏ Project Structure
Hereโs what Next.js creates for you:
my-awesome-app/
โโโ ๐ app/ # App directory (new routing)
โ โโโ ๐ layout.tsx # Root layout component
โ โโโ ๐ page.tsx # Home page
โ โโโ ๐ globals.css # Global styles
โโโ ๐ public/ # Static assets
โโโ ๐ next.config.js # Next.js configuration
โโโ ๐ tsconfig.json # TypeScript configuration
โโโ ๐ package.json # Dependencies
๐ง TypeScript Configuration
Next.js automatically generates a tsconfig.json
:
{
"compilerOptions": {
"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"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
๐ก Practical Examples
๐ Example 1: E-commerce Product Page
Letโs build something real - a type-safe product page:
// ๐ app/types/product.ts
export interface Product {
id: string;
name: string;
price: number;
description: string;
image: string;
category: "electronics" | "clothing" | "books" | "toys";
inStock: boolean;
rating: number;
emoji: string; // Every product needs an emoji!
}
export interface CartItem extends Product {
quantity: number;
}
// ๐ app/components/ProductCard.tsx
import Image from 'next/image';
import Link from 'next/link';
import { Product } from '@/app/types/product';
interface ProductCardProps {
product: Product;
onAddToCart: (product: Product) => void;
}
export default function ProductCard({ product, onAddToCart }: ProductCardProps) {
// ๐จ Format price with currency
const formatPrice = (price: number): string => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(price);
};
return (
<div className="bg-white rounded-lg shadow-md p-4 hover:shadow-lg transition-shadow">
{/* ๐ผ๏ธ Product image with Next.js optimization */}
<div className="relative h-48 mb-4">
<Image
src={product.image}
alt={product.name}
fill
className="object-cover rounded-md"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
{/* ๐ Product details */}
<div className="space-y-2">
<h3 className="text-lg font-semibold text-gray-900">
{product.emoji} {product.name}
</h3>
<p className="text-gray-600 text-sm line-clamp-2">
{product.description}
</p>
<div className="flex items-center justify-between">
<span className="text-xl font-bold text-green-600">
{formatPrice(product.price)}
</span>
<div className="flex items-center space-x-1">
<span className="text-yellow-400">โญ</span>
<span className="text-sm text-gray-600">
{product.rating.toFixed(1)}
</span>
</div>
</div>
{/* ๐ Add to cart button */}
<div className="flex space-x-2 mt-4">
<Link
href={`/products/${product.id}`}
className="flex-1 bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition-colors text-center"
>
View Details
</Link>
<button
onClick={() => onAddToCart(product)}
disabled={!product.inStock}
className={`px-4 py-2 rounded-md transition-colors ${
product.inStock
? 'bg-green-500 text-white hover:bg-green-600'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
>
{product.inStock ? '๐ Add' : 'โ Out of Stock'}
</button>
</div>
</div>
</div>
);
}
๐ฎ Example 2: API Route with TypeScript
Letโs create a type-safe API:
// ๐ app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Product } from '@/app/types/product';
// ๐ฏ Mock product data
const products: Product[] = [
{
id: '1',
name: 'Wireless Headphones',
price: 129.99,
description: 'High-quality wireless headphones with noise cancellation',
image: '/images/headphones.jpg',
category: 'electronics',
inStock: true,
rating: 4.5,
emoji: '๐ง'
},
{
id: '2',
name: 'TypeScript Handbook',
price: 39.99,
description: 'Complete guide to mastering TypeScript development',
image: '/images/book.jpg',
category: 'books',
inStock: true,
rating: 4.8,
emoji: '๐'
}
];
// ๐ GET handler - fetch all products
export async function GET(request: NextRequest) {
try {
// ๐ Optional filtering by category
const { searchParams } = new URL(request.url);
const category = searchParams.get('category') as Product['category'] | null;
let filteredProducts = products;
if (category) {
filteredProducts = products.filter(p => p.category === category);
}
return NextResponse.json({
success: true,
data: filteredProducts,
total: filteredProducts.length
});
} catch (error) {
console.error('โ Error fetching products:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch products' },
{ status: 500 }
);
}
}
// โ POST handler - add new product
export async function POST(request: NextRequest) {
try {
const body: Omit<Product, 'id'> = await request.json();
// ๐ก๏ธ Basic validation
if (!body.name || !body.price || !body.category) {
return NextResponse.json(
{ success: false, error: 'Missing required fields' },
{ status: 400 }
);
}
// ๐ Create new product
const newProduct: Product = {
...body,
id: Date.now().toString() // Simple ID generation
};
products.push(newProduct);
return NextResponse.json({
success: true,
data: newProduct
}, { status: 201 });
} catch (error) {
console.error('โ Error creating product:', error);
return NextResponse.json(
{ success: false, error: 'Failed to create product' },
{ status: 500 }
);
}
}
๐ฑ Example 3: Dynamic Routes with TypeScript
// ๐ app/products/[id]/page.tsx
import { notFound } from 'next/navigation';
import Image from 'next/image';
import { Product } from '@/app/types/product';
interface ProductPageProps {
params: {
id: string;
};
}
// ๐ Fetch product data
async function getProduct(id: string): Promise<Product | null> {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/products/${id}`, {
cache: 'no-store' // Always fetch fresh data
});
if (!res.ok) {
throw new Error('Failed to fetch product');
}
const data = await res.json();
return data.success ? data.data : null;
} catch (error) {
console.error('โ Error fetching product:', error);
return null;
}
}
export default async function ProductPage({ params }: ProductPageProps) {
const product = await getProduct(params.id);
// ๐ซ Show 404 if product not found
if (!product) {
notFound();
}
return (
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* ๐ผ๏ธ Product image */}
<div className="relative aspect-square">
<Image
src={product.image}
alt={product.name}
fill
className="object-cover rounded-lg"
priority
/>
</div>
{/* ๐ Product details */}
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">
{product.emoji} {product.name}
</h1>
<p className="text-xl text-green-600 font-semibold mt-2">
${product.price.toFixed(2)}
</p>
</div>
<div className="flex items-center space-x-2">
<div className="flex text-yellow-400">
{Array.from({ length: 5 }, (_, i) => (
<span key={i}>
{i < Math.floor(product.rating) ? 'โญ' : 'โ'}
</span>
))}
</div>
<span className="text-gray-600">
({product.rating} out of 5)
</span>
</div>
<p className="text-gray-700 leading-relaxed">
{product.description}
</p>
<div className="flex items-center space-x-4">
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
product.inStock
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{product.inStock ? 'โ
In Stock' : 'โ Out of Stock'}
</span>
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium capitalize">
๐ {product.category}
</span>
</div>
<button
disabled={!product.inStock}
className={`w-full py-3 px-6 rounded-lg font-medium transition-colors ${
product.inStock
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
>
{product.inStock ? '๐ Add to Cart' : 'โ Out of Stock'}
</button>
</div>
</div>
</div>
);
}
// ๐ Generate metadata for SEO
export async function generateMetadata({ params }: ProductPageProps) {
const product = await getProduct(params.id);
if (!product) {
return {
title: 'Product Not Found'
};
}
return {
title: `${product.name} - ${product.emoji}`,
description: product.description,
};
}
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Server Components vs Client Components
Understanding the distinction is crucial for optimal performance:
// ๐ app/components/ServerProductList.tsx
// ๐ฅ๏ธ Server Component (default in app directory)
import { Product } from '@/app/types/product';
async function getProducts(): Promise<Product[]> {
// ๐ This runs on the server
const res = await fetch('https://api.example.com/products');
return res.json();
}
export default async function ServerProductList() {
const products = await getProducts();
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{products.map(product => (
<div key={product.id} className="bg-white p-4 rounded-lg shadow">
<h3>{product.emoji} {product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
);
}
// ๐ app/components/ClientCounter.tsx
'use client'; // ๐ฏ This makes it a Client Component
import { useState } from 'react';
interface ClientCounterProps {
initialCount?: number;
}
export default function ClientCounter({ initialCount = 0 }: ClientCounterProps) {
const [count, setCount] = useState<number>(initialCount);
return (
<div className="flex items-center space-x-4">
<button
onClick={() => setCount(c => c - 1)}
className="bg-red-500 text-white px-4 py-2 rounded"
>
โ
</button>
<span className="text-2xl font-bold">{count}</span>
<button
onClick={() => setCount(c => c + 1)}
className="bg-green-500 text-white px-4 py-2 rounded"
>
โ
</button>
</div>
);
}
๐๏ธ Advanced Topic 2: Middleware with TypeScript
Create powerful middleware for authentication, logging, and more:
// ๐ middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// ๐ก๏ธ Define protected routes
const protectedRoutes = ['/dashboard', '/profile', '/admin'];
const authRoutes = ['/login', '/register'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const token = request.cookies.get('auth-token')?.value;
// ๐ Check if route is protected
const isProtectedRoute = protectedRoutes.some(route =>
pathname.startsWith(route)
);
const isAuthRoute = authRoutes.some(route =>
pathname.startsWith(route)
);
// ๐ซ Redirect unauthenticated users from protected routes
if (isProtectedRoute && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
// ๐ Redirect authenticated users from auth routes
if (isAuthRoute && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// ๐ Add custom headers for analytics
const response = NextResponse.next();
response.headers.set('x-pathname', pathname);
response.headers.set('x-timestamp', new Date().toISOString());
return response;
}
// ๐ฏ Configure which routes to run middleware on
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Mixing Server and Client Components
// โ Wrong way - trying to use hooks in Server Component
async function BadServerComponent() {
const [count, setCount] = useState(0); // ๐ฅ Error!
return <div>{count}</div>;
}
// โ
Correct way - separate concerns
// Server Component for data fetching
async function GoodServerComponent() {
const data = await fetchData();
return (
<div>
<ServerContent data={data} />
<ClientInteractiveComponent initialData={data} />
</div>
);
}
// Client Component for interactivity
'use client';
function ClientInteractiveComponent({ initialData }: { initialData: any }) {
const [count, setCount] = useState(0); // โ
Works!
return <div>{count}</div>;
}
๐คฏ Pitfall 2: Type Errors with Dynamic Routes
// โ Dangerous - params might not exist!
function BadProductPage({ params }: any) {
return <div>{params.id}</div>; // ๐ฅ No type safety!
}
// โ
Safe - proper typing with validation
interface ProductPageParams {
params: {
id: string;
};
}
function GoodProductPage({ params }: ProductPageParams) {
// ๐ก๏ธ TypeScript ensures params.id exists
if (!params.id) {
return <div>โ Product ID required</div>;
}
return <div>โ
Product: {params.id}</div>;
}
๐ ๏ธ Best Practices
- ๐ฏ Use TypeScript Strictly: Enable all strict mode options in
tsconfig.json
- ๐ Type Your API Routes: Define interfaces for request/response bodies
- ๐ก๏ธ Validate Input Data: Never trust data from forms or API calls
- ๐ Optimize Images: Always use Next.js
Image
component with proper sizing - โจ Keep Components Pure: Separate data fetching from presentation logic
- ๐ง Use Environment Variables: Keep sensitive data secure with
.env.local
- ๐ Monitor Performance: Use Next.js built-in analytics and profiling tools
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Blog System with TypeScript
Create a complete blog system with posts, comments, and user authentication:
๐ Requirements:
- โ Blog posts with title, content, author, and publish date
- ๐ท๏ธ Categories and tags for posts
- ๐ฌ Comment system with nested replies
- ๐ค User authentication and profiles
- ๐ Search functionality with filters
- ๐จ Each post needs an emoji and category color!
๐ Bonus Points:
- Add server-side rendering for SEO
- Implement real-time comments with WebSockets
- Create an admin dashboard for content management
- Add social sharing features
๐ก Solution
๐ Click to see solution
// ๐ app/types/blog.ts
export interface User {
id: string;
username: string;
email: string;
avatar?: string;
role: 'admin' | 'editor' | 'author' | 'reader';
createdAt: Date;
}
export interface BlogPost {
id: string;
title: string;
slug: string;
content: string;
excerpt: string;
author: User;
category: BlogCategory;
tags: string[];
emoji: string;
publishedAt: Date;
updatedAt: Date;
published: boolean;
views: number;
likes: number;
}
export interface BlogCategory {
id: string;
name: string;
slug: string;
description: string;
color: string;
emoji: string;
}
export interface Comment {
id: string;
content: string;
author: User;
postId: string;
parentId?: string; // For nested replies
createdAt: Date;
likes: number;
}
// ๐ app/components/BlogPostCard.tsx
import Link from 'next/link';
import Image from 'next/image';
import { BlogPost } from '@/app/types/blog';
interface BlogPostCardProps {
post: BlogPost;
}
export function BlogPostCard({ post }: BlogPostCardProps) {
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
};
return (
<article className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
<div className="p-6">
{/* ๐ท๏ธ Category badge */}
<div className="flex items-center justify-between mb-4">
<span
className="px-3 py-1 rounded-full text-sm font-medium"
style={{
backgroundColor: `${post.category.color}20`,
color: post.category.color
}}
>
{post.category.emoji} {post.category.name}
</span>
<div className="flex items-center text-sm text-gray-500 space-x-4">
<span>๐๏ธ {post.views}</span>
<span>โค๏ธ {post.likes}</span>
</div>
</div>
{/* ๐ Post title and excerpt */}
<Link href={`/blog/${post.slug}`}>
<h2 className="text-xl font-bold text-gray-900 mb-2 hover:text-blue-600 transition-colors">
{post.emoji} {post.title}
</h2>
</Link>
<p className="text-gray-600 mb-4 line-clamp-2">
{post.excerpt}
</p>
{/* ๐ค Author and date */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
{post.author.avatar && (
<Image
src={post.author.avatar}
alt={post.author.username}
width={32}
height={32}
className="rounded-full"
/>
)}
<div>
<p className="text-sm font-medium text-gray-900">
{post.author.username}
</p>
<p className="text-xs text-gray-500">
๐
{formatDate(post.publishedAt)}
</p>
</div>
</div>
<Link
href={`/blog/${post.slug}`}
className="text-blue-600 hover:text-blue-800 font-medium text-sm"
>
Read More โ
</Link>
</div>
{/* ๐ท๏ธ Tags */}
{post.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-4">
{post.tags.slice(0, 3).map(tag => (
<span
key={tag}
className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded-md"
>
#{tag}
</span>
))}
{post.tags.length > 3 && (
<span className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded-md">
+{post.tags.length - 3} more
</span>
)}
</div>
)}
</div>
</article>
);
}
// ๐ app/api/blog/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { BlogPost, BlogCategory } from '@/app/types/blog';
// ๐ฏ Mock data for demonstration
const categories: BlogCategory[] = [
{
id: '1',
name: 'TypeScript',
slug: 'typescript',
description: 'All about TypeScript development',
color: '#3178c6',
emoji: '๐'
},
{
id: '2',
name: 'Next.js',
slug: 'nextjs',
description: 'Next.js tutorials and tips',
color: '#000000',
emoji: '๐'
}
];
const mockPosts: BlogPost[] = [
{
id: '1',
title: 'Getting Started with Next.js and TypeScript',
slug: 'getting-started-nextjs-typescript',
content: 'Full blog post content here...',
excerpt: 'Learn how to build modern web applications with Next.js and TypeScript',
author: {
id: '1',
username: 'john_dev',
email: '[email protected]',
role: 'author',
createdAt: new Date()
},
category: categories[0],
tags: ['nextjs', 'typescript', 'react', 'tutorial'],
emoji: '๐',
publishedAt: new Date('2024-01-15'),
updatedAt: new Date('2024-01-15'),
published: true,
views: 1250,
likes: 42
}
];
// ๐ GET handler - fetch blog posts with filtering
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const category = searchParams.get('category');
const tag = searchParams.get('tag');
const search = searchParams.get('search');
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
let filteredPosts = mockPosts.filter(post => post.published);
// ๐ท๏ธ Filter by category
if (category) {
filteredPosts = filteredPosts.filter(
post => post.category.slug === category
);
}
// ๐ Filter by tag
if (tag) {
filteredPosts = filteredPosts.filter(
post => post.tags.includes(tag)
);
}
// ๐ Search in title and content
if (search) {
const searchLower = search.toLowerCase();
filteredPosts = filteredPosts.filter(
post =>
post.title.toLowerCase().includes(searchLower) ||
post.content.toLowerCase().includes(searchLower)
);
}
// ๐ Pagination
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedPosts = filteredPosts.slice(startIndex, endIndex);
return NextResponse.json({
success: true,
data: {
posts: paginatedPosts,
pagination: {
page,
limit,
total: filteredPosts.length,
totalPages: Math.ceil(filteredPosts.length / limit)
}
}
});
} catch (error) {
console.error('โ Error fetching blog posts:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch blog posts' },
{ status: 500 }
);
}
}
๐ Key Takeaways
Youโve learned so much about Next.js with TypeScript! Hereโs what you can now do:
- โ Set up Next.js projects with TypeScript configuration ๐ช
- โ Build type-safe components and API routes ๐ก๏ธ
- โ Handle dynamic routing with proper type checking ๐ฏ
- โ Optimize performance with Server and Client Components ๐
- โ Deploy production-ready full-stack applications! ๐
Remember: Next.js and TypeScript together create an amazing developer experience that scales from small projects to enterprise applications! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Next.js with TypeScript!
Hereโs what to do next:
- ๐ป Build a full-stack project using the concepts above
- ๐๏ธ Explore advanced patterns like custom hooks and context
- ๐ Move on to our next tutorial: React Functional Components Type Safety
- ๐ Share your Next.js creations with the community!
Remember: Every Next.js expert was once a beginner. Keep building, keep learning, and most importantly, have fun creating amazing web applications! ๐
Happy coding! ๐๐โจ