SSR Mini Project — Build a Complete Server-Side Rendered Application
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
- Not implementing caching early. Without caching, every request renders the full page. Implement Redis caching for public pages from the start.
- No streaming for slow sections. Product detail pages with reviews, recommendations, and related items should use streaming to show content progressively.
- 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.
- Missing authentication checks. Admin routes, cart, and checkout must check authentication. Use middleware for global checks and per-route checks for specific pages.
- No performance monitoring. Without metrics, you do not know if your optimization works. Track SSR render time, cache hit ratio, and TTFB.
Practice Questions
- What architectural decisions did you make for this project and why?
- How did you implement caching for SSR pages?
- How does streaming improve the product detail page experience?
- What security measures did you implement and why?
- 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
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