Skip to content

SSR Mini Project — Build a Complete Server-Side Rendered Application

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about SSR Mini Project. We cover key concepts, practical examples, and best practices to help you master this topic.

Build a complete SSR application from scratch combining React SSR, data fetching, caching, streaming, security, API routes, middleware, and deployment into one production-ready application.

What You'll Learn

By the end of this project, you will have built a production-ready SSR application that incorporates all the concepts from this tutorial series: server-side rendering with renderToString, streaming with Suspense, data fetching with caching, API routes, authentication middleware, performance optimization, and deployment.

Why It Matters

Reading tutorials teaches concepts, but building a complete project solidifies your understanding. This capstone project simulates a real-world development process where you must make architectural decisions, integrate multiple concerns, debug issues, and deliver a working SSR application to production.

Real-World Use

This project mirrors the architecture of production SSR applications like Vercel's documentation, Airbnb's listing pages, and Nike's product pages — content-rich, SEO-critical, and personalized. Completing this project prepares you to build real-world SSR applications.

SSR Capstone Architecture
    ┌──────────────────────────────────────────────────────────┐
    │          E-Commerce SSR Application Architecture        │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  Pages:                                                  │
    │  ├── Home (SSR with streaming)                          │
    │  ├── Products (SSR with Redis caching)                  │
    │  ├── Product/[id] (SSR with Suspense streaming)         │
    │  ├── Cart (SSR with user session)                       │
    │  ├── Checkout (SSR with auth middleware)                │
    │  ├── Search (SSR with API route)                        │
    │  └── Admin (SSR with auth guard)                        │
    │                                                          │
    │  API Routes:                                             │
    │  ├── /api/products (CRUD)                               │
    │  ├── /api/cart (client-side interaction)                │
    │  └── /api/auth (login, register)                        │
    │                                                          │
    │  Middleware: Auth, Logging, Caching, Security           │
    │                                                          │
    │  Infrastructure: PM2, Nginx, Redis, PostgreSQL          │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

Think of this project like building a house after learning carpentry, plumbing, and electrical work separately. Now you put it all together. Each skill you learned — SSR rendering, caching, security, API routes — is a trade that contributes to the final structure.

Project Overview

Build an E-Commerce SSR Application with the following features:

  • Home page with featured products (streaming SSR with Suspense)
  • Product listing with search and filters (Redis cached SSR)
  • Product detail with reviews (Suspense streaming for slow sections)
  • Shopping cart with add/remove (client-side + API route)
  • User authentication with session middleware
  • Admin dashboard with auth guard middleware
  • Performance: Redis caching, CDN headers, streaming
  • Security: CSP headers, input validation, Rate Limiting

Step 1: Project Setup

# Create Next.js app (Pages Router for simplicity)
npx create-next-app@latest ecommerce-ssr --use-npm
cd ecommerce-ssr

# Install dependencies
npm install redis react-query
npm install bcrypt jsonwebtoken cookie-parser
npm install -D @types/redis

# Structure:
# pages/
#   index.js          — Home page (SSR with streaming)
#   products/
#     index.js        — Product listing (SSR cached)
#     [id].js         — Product detail (Suspense SSR)
#   cart.js           — Cart page (SSR + client)
#   api/
#     products/
#       index.js      — Products API
#     cart.js         — Cart API
#     auth.js         — Authentication API
# middleware.js       — Auth, logging, caching
# lib/
#   cache.js          — Redis cache helpers
#   db.js             — Database functions
#   auth.js           — Auth helpers

Step 2: SSR Product Listing with Caching

// pages/products/index.js
import { useState } from 'react';
import Link from 'next/link';

export default function Products({ products, timestamp }) {
    const [search, setSearch] = useState('');

    const filtered = products.filter(p =>
        p.name.toLowerCase().includes(search.toLowerCase())
    );

    return (
        <div>
            <h1>Products</h1>
            <p>Cached at: {timestamp}</p>

            <input
                type="search"
                placeholder="Filter products..."
                value={search}
                onChange={(e) => setSearch(e.target.value)}
            />

            <div className="grid">
                {filtered.map(product => (
                    <div key={product.id}>
                        <Link href={`/products/${product.id}`}>
                            {product.name}
                        </Link>
                        <p>${product.price}</p>
                    </div>
                ))}
            </div>
        </div>
    );
}

export async function getServerSideProps({ req, res }) {
    // Check Redis cache
    const cacheKey = 'ssr:products';
    const cache = await redis.get(cacheKey);

    if (cache) {
        res.setHeader('X-Cache', 'HIT');
        return { props: { products: JSON.parse(cache), timestamp: 'cached' } };
    }

    // Fetch from database
    const products = await db.products.findAll();

    // Store in Redis cache
    await redis.setEx(cacheKey, 60, JSON.stringify(products));
    res.setHeader('X-Cache', 'MISS');

    return {
        props: {
            products,
            timestamp: new Date().toISOString()
        }
    };
}

Step 3: Streaming Product Detail with Suspense

// pages/products/[id].js
import { Suspense } from 'react';
import { useRouter } from 'next/router';

function ProductInfo({ product }) {
    return (
        <div>
            <h1>{product.name}</h1>
            <p>{product.description}</p>
            <p className="price">${product.price}</p>
        </div>
    );
}

function ProductReviews({ productId }) {
    // This could stream in slowly
    const [reviews, setReviews] = useState([]);

    useEffect(() => {
        fetch(`/api/products/${productId}/reviews`)
            .then(r => r.json())
            .then(setReviews);
    }, [productId]);

    return (
        <div>
            <h2>Reviews</h2>
            {reviews.map(review => (
                <div key={review.id}>
                    <p>{review.text}</p>
                    <small> {review.author}</small>
                </div>
            ))}
        </div>
    );
}

export default function ProductDetail({ product }) {
    const router = useRouter();
    const { id } = router.query;

    return (
        <div>
            <ProductInfo product={product} />

            <Suspense fallback={<div>Loading reviews...</div>}>
                <ProductReviews productId={id} />
            </Suspense>
        </div>
    );
}

export async function getServerSideProps({ params }) {
    const product = await db.products.findById(params.id);

    if (!product) {
        return { notFound: true };
    }

    return {
        props: { product }
    };
}

Step 4: API Route with Authentication

// pages/api/cart.js
import { getSession } from '../../lib/auth';

export default async function handler(req, res) {
    const session = await getSession(req);

    if (!session) {
        return res.status(401).json({ error: 'Authentication required' });
    }

    switch (req.method) {
        case 'GET':
            const cart = await db.cart.findByUser(session.userId);
            return res.status(200).json(cart);

        case 'POST':
            const { productId, quantity } = req.body;

            if (!productId || !quantity) {
                return res.status(422).json({ error: 'productId and quantity required' });
            }

            const updated = await db.cart.addItem(session.userId, productId, quantity);
            return res.status(200).json(updated);

        case 'DELETE':
            const deleteId = req.body.productId;
            await db.cart.removeItem(session.userId, deleteId);
            return res.status(200).json({ success: true });

        default:
            res.setHeader('Allow', ['GET', 'POST', 'DELETE']);
            return res.status(405).end(`Method ${req.method} Not Allowed`);
    }
}

Step 5: Middleware

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
    const { pathname } = request.nextUrl;
    const response = NextResponse.next();

    // Logging
    console.log(`[${new Date().toISOString()}] ${request.method} ${pathname}`);

    // Security headers
    response.headers.set('X-Frame-Options', 'DENY');
    response.headers.set('X-Content-Type-Options', 'nosniff');
    response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
    response.headers.set(
        'Content-Security-Policy',
        "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"
    );

    // Admin route protection
    if (pathname.startsWith('/admin')) {
        const token = request.cookies.get('token');
        if (!token) {
            return NextResponse.redirect(new URL('/login', request.url));
        }
    }

    // Cache public pages
    if (pathname.startsWith('/products') || pathname === '/') {
        response.headers.set(
            'Cache-Control',
            'public, s-maxage=60, stale-while-revalidate=30'
        );
    }

    return response;
}

export const config = {
    matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
};

Common Mistakes

  1. Not implementing caching early. Without caching, every request renders the full page. Implement Redis caching for public pages from the start.
  2. No streaming for slow sections. Product detail pages with reviews, recommendations, and related items should use streaming to show content progressively.
  3. Not separating SSR and client data fetching. Use API routes for client-side interactions and the same data functions for SSR. Avoid duplicate data fetching logic.
  4. Missing authentication checks. Admin routes, cart, and checkout must check authentication. Use middleware for global checks and per-route checks for specific pages.
  5. No performance monitoring. Without metrics, you do not know if your optimization works. Track SSR render time, cache hit ratio, and TTFB.

Practice Questions

  1. What architectural decisions did you make for this project and why?
  2. How did you implement caching for SSR pages?
  3. How does streaming improve the product detail page experience?
  4. What security measures did you implement and why?
  5. How did you handle the connection between SSR and client-side interactivity (cart)?

Challenge: Extend the E-Commerce SSR application with: search with Redis caching (search results cached for 30 seconds), product recommendations (streamed via dedicated Suspense boundary), user order history (SSR with authentication middleware), real-time stock updates via Websocket, image optimization with Next.js Image component, and load testing with k6 to verify 1000 RPS.

FAQ

What is the best way to learn SSR?

Build projects. Start with a simple SSR app using Express and React renderToString, then add streaming, caching, and authentication. Progress to frameworks like Next.js for production.

Should I use Next.js or custom Express SSR?

Use Next.js for most projects. It handles routing, code splitting, caching, and deployment. Use custom Express SSR for learning or when you need full control over the server.

What is the most important concept in SSR?

Understanding the server-client boundary: what runs on the server, what runs on the client, and how data flows between them. Hydration is the bridge.

How do I decide between SSR, SSG, and ISR?

SSR for dynamic, user-specific content. SSG for static content that does not change often. ISR for content that changes periodically and needs fast rebuilds.

How do I keep learning after this project?

Contribute to open-source Next.js projects, explore Remix and SvelteKit for different SSR approaches, learn about edge SSR, and study real-world SSR architectures.

Mini Project

The project you just built IS the mini project. Extend it with: Redis caching for all public pages, streaming product detail with Suspense for reviews and recommendations, API routes for cart operations with authentication, middleware for security headers and logging, image optimization, load testing with 1000 concurrent users, and deployment to Vercel or a VPS with PM2 and Nginx.

What's Next

You have completed the SSR tutorial series. Explore related topics: Static Site Generation to learn when pre-rendering at build time beats SSR, or Incremental Static Regeneration for the best of both static and dynamic rendering.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro