Next.js

Building a Full Next.js Application - Part 2

20 min Lesson 40 of 80

Building a Full Next.js Application - Part 2

Welcome to the final lesson! In this comprehensive lesson, we'll complete our blog application with advanced features, implement testing, prepare for production deployment, and review everything you've learned throughout this tutorial series.

Adding Advanced Features

Let's enhance our application with professional features that improve user experience and functionality.

1. Search Functionality

// app/search/page.tsx import { Suspense } from 'react'; import { Metadata } from 'next'; import SearchResults from '@/components/SearchResults'; import SearchForm from '@/components/SearchForm'; export const metadata: Metadata = { title: 'Search Posts', description: 'Search through our blog posts', }; export default function SearchPage({ searchParams, }: { searchParams: { q?: string }; }) { const query = searchParams.q || ''; return ( <div className="container"> <h1>Search Posts</h1> <SearchForm initialQuery={query} /> <Suspense fallback={<div>Searching...</div>}> <SearchResults query={query} /> </Suspense> </div> ); }
// components/SearchForm.tsx 'use client'; import { useState, useTransition } from 'react'; import { useRouter } from 'next/navigation'; export default function SearchForm({ initialQuery = '', }: { initialQuery?: string; }) { const [query, setQuery] = useState(initialQuery); const [isPending, startTransition] = useTransition(); const router = useRouter(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); startTransition(() => { router.push(`/search?q=${encodeURIComponent(query)}`); }); }; return ( <form onSubmit={handleSubmit} className="search-form"> <input type="search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search posts..." disabled={isPending} /> <button type="submit" disabled={isPending || !query.trim()}> {isPending ? 'Searching...' : 'Search'} </button> </form> ); }
// components/SearchResults.tsx import { getPosts } from '@/lib/blog'; import PostCard from './PostCard'; export default async function SearchResults({ query, }: { query: string; }) { if (!query.trim()) { return <p>Enter a search term to find posts.</p>; } const posts = await getPosts(); const filteredPosts = posts.filter(post => post.title.toLowerCase().includes(query.toLowerCase()) || post.excerpt.toLowerCase().includes(query.toLowerCase()) || post.tags.some(tag => tag.toLowerCase().includes(query.toLowerCase())) ); if (filteredPosts.length === 0) { return <p>No posts found for "{query}"</p>; } return ( <div className="search-results"> <h2>{filteredPosts.length} results for "{query}"</h2> <div className="posts-grid"> {filteredPosts.map(post => ( <PostCard key={post.slug} post={post} /> ))} </div> </div> ); }

2. Reading Progress Indicator

// components/ReadingProgress.tsx 'use client'; import { useEffect, useState } from 'react'; export default function ReadingProgress() { const [progress, setProgress] = useState(0); useEffect(() => { const updateProgress = () => { const scrollTop = window.scrollY; const docHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollPercent = (scrollTop / docHeight) * 100; setProgress(scrollPercent); }; window.addEventListener('scroll', updateProgress); return () => window.removeEventListener('scroll', updateProgress); }, []); return ( <div className="reading-progress" style={{ position: 'fixed', top: 0, left: 0, width: `${progress}%`, height: '3px', backgroundColor: 'var(--primary-color)', zIndex: 9999, transition: 'width 0.1s ease-out', }} /> ); }

3. Table of Contents

// components/TableOfContents.tsx 'use client'; import { useEffect, useState } from 'react'; interface Heading { id: string; text: string; level: number; } export default function TableOfContents() { const [headings, setHeadings] = useState<Heading[]>([]); const [activeId, setActiveId] = useState<string>(''); useEffect(() => { const elements = Array.from( document.querySelectorAll('article h2, article h3') ); const headingsData = elements.map(elem => ({ id: elem.id, text: elem.textContent || '', level: Number(elem.tagName.charAt(1)), })); setHeadings(headingsData); // Intersection Observer for active heading const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setActiveId(entry.target.id); } }); }, { rootMargin: '-100px 0px -80% 0px' } ); elements.forEach((elem) => observer.observe(elem)); return () => observer.disconnect(); }, []); if (headings.length === 0) return null; return ( <nav className="table-of-contents"> <h3>Table of Contents</h3> <ul> {headings.map((heading) => ( <li key={heading.id} style={{ marginLeft: `${(heading.level - 2) * 1}rem` }} > <a href={`#${heading.id}`} className={activeId === heading.id ? 'active' : ''} onClick={(e) => { e.preventDefault(); document.getElementById(heading.id)?.scrollIntoView({ behavior: 'smooth', }); }} > {heading.text} </a> </li> ))} </ul> </nav> ); }
Note: These features enhance user experience significantly. The reading progress bar provides visual feedback, table of contents improves navigation, and search helps users find relevant content quickly.

Testing Your Next.js Application

Implementing comprehensive testing ensures your application works correctly and prevents regressions.

1. Unit Testing with Jest

// Install dependencies npm install --save-dev jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom // jest.config.js const nextJest = require('next/jest'); const createJestConfig = nextJest({ dir: './', }); const customJestConfig = { setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], testEnvironment: 'jest-environment-jsdom', moduleNameMapper: { '^@/(.*)$': '<rootDir>/$1', }, }; module.exports = createJestConfig(customJestConfig);
// jest.setup.js import '@testing-library/jest-dom'; // __tests__/components/PostCard.test.tsx import { render, screen } from '@testing-library/react'; import PostCard from '@/components/PostCard'; describe('PostCard', () => { const mockPost = { slug: 'test-post', title: 'Test Post', date: '2024-01-01', excerpt: 'Test excerpt', image: '/test.jpg', author: 'John Doe', tags: ['test'], }; it('renders post information correctly', () => { render(<PostCard post={mockPost} />); expect(screen.getByText('Test Post')).toBeInTheDocument(); expect(screen.getByText('Test excerpt')).toBeInTheDocument(); expect(screen.getByText('John Doe')).toBeInTheDocument(); }); it('links to the correct post URL', () => { render(<PostCard post={mockPost} />); const link = screen.getByRole('link'); expect(link).toHaveAttribute('href', '/blog/test-post'); }); });

2. End-to-End Testing with Playwright

// Install Playwright npm init playwright@latest // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, });
// e2e/blog.spec.ts import { test, expect } from '@playwright/test'; test.describe('Blog functionality', () => { test('should display blog posts on homepage', async ({ page }) => { await page.goto('/'); await expect(page.locator('h1')).toContainText('My Blog'); await expect(page.locator('.post-card')).toHaveCount(3); }); test('should navigate to post detail page', async ({ page }) => { await page.goto('/'); const firstPost = page.locator('.post-card').first(); const postTitle = await firstPost.locator('h2').textContent(); await firstPost.locator('a').click(); await expect(page.locator('article h1')).toContainText(postTitle!); }); test('should search for posts', async ({ page }) => { await page.goto('/search'); await page.fill('input[type="search"]', 'Next.js'); await page.click('button[type="submit"]'); await expect(page.locator('.search-results')).toBeVisible(); await expect(page.locator('.post-card')).toHaveCount.greaterThan(0); }); test('should submit comment form', async ({ page }) => { await page.goto('/blog/first-post'); await page.fill('input[name="name"]', 'Test User'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('textarea[name="comment"]', 'Great post!'); await page.click('button[type="submit"]'); await expect(page.locator('.success-message')).toBeVisible(); }); });
Tip: Run tests before deploying: npm test for Jest, npx playwright test for E2E tests. Set up CI/CD to run tests automatically on every commit.

Production Optimization

1. Environment Variables

// .env.local (development) DATABASE_URL=postgresql://localhost:5432/mydb NEXT_PUBLIC_API_URL=http://localhost:3000/api NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX // .env.production (production) DATABASE_URL=postgresql://production-host/mydb NEXT_PUBLIC_API_URL=https://myapp.com/api NEXT_PUBLIC_GA_ID=G-YYYYYYYYYY
Warning: Never commit .env.local or .env.production to version control. Add them to .gitignore. Only variables prefixed with NEXT_PUBLIC_ are exposed to the browser.

2. Performance Optimization Checklist

// next.config.js - Production optimizations /** @type {import('next').NextConfig} */ const nextConfig = { // Compress output compress: true, // Optimize images images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], }, // Bundle analyzer (comment out in production) // webpack: (config, { isServer }) => { // if (!isServer) { // const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); // config.plugins.push( // new BundleAnalyzerPlugin({ // analyzerMode: 'static', // openAnalyzer: false, // }) // ); // } // return config; // }, // Production-only features swcMinify: true, reactStrictMode: true, poweredByHeader: false, // Security headers async headers() { return [ { source: '/:path*', headers: [ { key: 'X-DNS-Prefetch-Control', value: 'on', }, { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload', }, { key: 'X-Content-Type-Options', value: 'nosniff', }, { key: 'X-Frame-Options', value: 'DENY', }, { key: 'Referrer-Policy', value: 'origin-when-cross-origin', }, ], }, ]; }, }; module.exports = nextConfig;

Deployment Guide

1. Deploy to Vercel (Recommended)

# Install Vercel CLI npm install -g vercel # Login to Vercel vercel login # Deploy vercel # Deploy to production vercel --prod

Vercel offers:

  • Automatic HTTPS and SSL certificates
  • Global CDN for static assets
  • Automatic deployments from Git
  • Preview deployments for pull requests
  • Built-in analytics and monitoring

2. Deploy to Other Platforms

# Build for production npm run build # Start production server npm start # Or export as static site (if applicable) npm run build && npx next export
Deployment Options:
  • Vercel: Best for Next.js, zero configuration
  • Netlify: Good alternative, easy setup
  • AWS Amplify: AWS integration, scalable
  • Docker: Self-hosted, full control
  • DigitalOcean App Platform: Simple and affordable

3. Docker Deployment

# Dockerfile FROM node:18-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci --only=production FROM node:18-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM node:18-alpine AS runner WORKDIR /app ENV NODE_ENV production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT 3000 CMD ["node", "server.js"]
# Build and run Docker container docker build -t my-nextjs-app . docker run -p 3000:3000 my-nextjs-app

Production Checklist

✅ Pre-Deployment Checklist: [ ] All tests passing (unit + E2E) [ ] Environment variables configured [ ] Database migrations run [ ] Error tracking set up (Sentry, LogRocket) [ ] Analytics configured (Google Analytics, Plausible) [ ] SEO metadata complete [ ] Sitemap generated [ ] robots.txt configured [ ] Security headers configured [ ] HTTPS enabled [ ] Image optimization verified [ ] Performance tested (Lighthouse score >90) [ ] Accessibility tested (WCAG 2.1 AA) [ ] Mobile responsiveness verified [ ] Cross-browser testing complete [ ] Backup strategy in place [ ] Monitoring and alerts configured

Monitoring and Maintenance

// lib/monitoring.ts export function reportWebVitals(metric: any) { // Send to analytics if (typeof window.gtag !== 'undefined') { window.gtag('event', metric.name, { value: Math.round(metric.value), event_label: metric.id, non_interaction: true, }); } // Log to console in development if (process.env.NODE_ENV === 'development') { console.log(metric); } } // app/layout.tsx - Add this export export { reportWebVitals } from '@/lib/monitoring';

Course Review: What You've Learned

Congratulations on completing this comprehensive Next.js tutorial! Let's review everything you've mastered:

Core Concepts (Lessons 1-10)

  • Next.js Fundamentals: App Router, file-based routing, server vs client components
  • Routing: Dynamic routes, nested layouts, route groups, parallel routes
  • Data Fetching: Server-side, client-side, streaming, suspense boundaries
  • Rendering: SSR, SSG, ISR, client-side rendering strategies

Advanced Features (Lessons 11-20)

  • API Routes: RESTful APIs, serverless functions, middleware
  • Authentication: NextAuth.js, session management, protected routes
  • Database Integration: Prisma ORM, database queries, relationships
  • Image Optimization: Next/Image component, automatic optimization

Professional Development (Lessons 21-30)

  • State Management: Context API, Zustand, server state
  • Forms: React Hook Form, validation, file uploads
  • Styling: CSS Modules, Tailwind CSS, styled-components
  • Performance: Code splitting, lazy loading, caching strategies

Production Skills (Lessons 31-40)

  • SEO: Metadata API, OpenGraph, JSON-LD, sitemap generation
  • Testing: Unit tests (Jest), E2E tests (Playwright)
  • Deployment: Vercel, Docker, CI/CD pipelines
  • Monitoring: Web Vitals, error tracking, analytics

Next Steps: Continue Your Journey

  1. Build Real Projects: Apply what you've learned to real-world applications
  2. Explore Advanced Topics: Incremental Static Regeneration, Edge Functions, Middleware patterns
  3. Join the Community: GitHub, Discord, Stack Overflow, Reddit r/nextjs
  4. Stay Updated: Follow Next.js blog, release notes, and RFC discussions
  5. Contribute: Open source contributions, write blog posts, create tutorials

Recommended Resources

  • Official Documentation: nextjs.org/docs - Always up-to-date
  • Next.js Examples: github.com/vercel/next.js/tree/canary/examples
  • Learn Platform: nextjs.org/learn - Interactive tutorials
  • YouTube Channels: Vercel, Lee Robinson, Fireship
  • Courses: Frontend Masters, Egghead.io, Udemy
  • Books: "The Next.js Handbook", "React and Next.js"
Final Project Challenge:
  1. Complete the blog application with all features from lessons 39-40
  2. Add at least 3 custom features (e.g., bookmarks, categories, related posts)
  3. Implement comprehensive testing (80%+ code coverage)
  4. Deploy to production with custom domain
  5. Achieve Lighthouse scores: Performance 90+, Accessibility 100, Best Practices 100, SEO 100
  6. Document your code and write a README
  7. Share your project on GitHub and social media
Final Tips for Success:
  • Always read error messages carefully - they usually tell you exactly what's wrong
  • Use TypeScript for better code quality and developer experience
  • Profile before optimizing - don't guess what's slow
  • Write tests for critical functionality
  • Keep dependencies updated and monitor security vulnerabilities
  • Learn from others' code - read popular open-source Next.js projects
  • Don't over-engineer - start simple and add complexity as needed

Thank You!

Thank you for completing this comprehensive Next.js tutorial! You now have the skills to build modern, production-ready web applications with Next.js. Remember that learning is a continuous journey - keep practicing, building, and sharing your knowledge with others.

The web development community is welcoming and supportive. Don't hesitate to ask questions, share your work, and help others on their journey. Good luck with your Next.js projects, and happy coding!

Final Note: This tutorial covered Next.js 14 with the App Router. Next.js evolves rapidly, so always refer to the official documentation for the latest features and best practices. The fundamentals you've learned here will remain relevant across versions.