SSR API Routes — Building API Endpoints Within SSR Frameworks
In this tutorial, you will learn about SSR API Routes. We cover key concepts, practical examples, and best practices to help you master this topic.
SSR API routes allow building server-side API endpoints alongside SSR pages in frameworks like Next.js, providing data to both the SSR rendering pipeline and client-side JavaScript.
What You'll Learn
By the end of this tutorial, you will understand how SSR API routes work, how to create API routes in Next.js Pages Router and App Router, how to handle CRUD operations, how to secure API routes with authentication and validation, and how to use API routes for both SSR data fetching and client-side requests.
Why It Matters
In SSR applications, you need server-side endpoints for two purposes: fetching data during SSR rendering and providing data to client-side JavaScript after hydration. API routes unify these into a single interface, keeping your data logic in one place.
Real-World Use
A Next.js e-commerce site uses API routes for both SSR (product page fetches data via getServerSideProps calling the API route) and client-side interactions (adding to cart, searching, filtering). The API route handles authorization, validation, and data formatting in one place.
SSR API Route Architecture
┌──────────────────────────────────────────────────────────┐
│ SSR + API Route Architecture │
├──────────────────────────────────────────────────────────┤
│ │
│ Server Request │
│ │ │
│ ├──> SSR Route (pages/product/[id].js) │
│ │ │ │
│ │ └──> API Route (pages/api/products/[id].js) │
│ │ │ │
│ │ └──> Database / External API │
│ │ │
│ ├──> Static assets (images, CSS) │
│ │ │
│ Browser Request (client-side interaction) │
│ │ │
│ └──> fetch('/api/products/[id]') │
│ │ │
│ └──> Same API Route as SSR │
│ │ │
│ └──> Same Database / External API │
│ │
│ Benefits: │
│ • Unified data layer (SSR and client use same API) │
│ • Authentication handled once │
│ • Validation logic centralized │
│ • Caching configured per endpoint │
│ │
└──────────────────────────────────────────────────────────┘
Think of SSR API routes like a restaurant where the same kitchen prepares food for both dine-in (SSR) and takeout (client API). The recipes (data logic) and ingredients (database) are the same. The only difference is how the food is packaged (wrapped in HTML for SSR, served as JSON for API).
Next.js API Routes
// pages/api/products/index.js — Product listing API
export default async function handler(req, res) {
switch (req.method) {
case 'GET':
return getProducts(req, res);
case 'POST':
return createProduct(req, res);
default:
res.setHeader('Allow', ['GET', 'POST']);
return res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
async function getProducts(req, res) {
const { page = 1, limit = 10, category } = req.query;
try {
const products = await db.products.findAll({
page: parseInt(page),
limit: parseInt(limit),
category
});
// Cache for 60 seconds at the CDN level
res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=30');
res.status(200).json(products);
} catch (error) {
console.error('Failed to fetch products:', error);
res.status(500).json({ error: 'Failed to fetch products' });
}
}
async function createProduct(req, res) {
const { name, price, category, description } = req.body;
// Validation
const errors = [];
if (!name || name.length < 2) errors.push('Name is required (min 2 chars)');
if (!price || isNaN(price) || price <= 0) errors.push('Valid price is required');
if (errors.length > 0) {
return res.status(422).json({ errors });
}
try {
const product = await db.products.create({
name, price, category, description
});
res.status(201).json(product);
} catch (error) {
console.error('Failed to create product:', error);
res.status(500).json({ error: 'Failed to create product' });
}
}
// Using API route in getServerSideProps
export async function getServerSideProps() {
// Call the API route internally (or use the same data function)
const products = await db.products.findAll({ limit: 20 });
return {
props: { products }
};
}
App Router API Routes
// app/api/products/route.js — Next.js App Router API route
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
export async function GET(request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page')) || 1;
const limit = parseInt(searchParams.get('limit')) || 10;
try {
const products = await db.products.findAll({ page, limit });
return NextResponse.json(products, {
status: 200,
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30'
}
});
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch products' },
{ status: 500 }
);
}
}
export async function POST(request) {
const body = await request.json();
const { name, price } = body;
// Validate
if (!name || typeof name !== 'string') {
return NextResponse.json(
{ error: 'Name is required' },
{ status: 422 }
);
}
try {
const product = await db.products.create(body);
return NextResponse.json(product, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create product' },
{ status: 500 }
);
}
}
// Using in a Server Component
// app/products/page.js
async function ProductsPage() {
// Fetch directly (not through HTTP)
const products = await db.products.findAll({ limit: 20 });
return (
<div>
<h1>Products</h1>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
Securing API Routes
// middleware.js — Apply middleware to API routes
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
export async function middleware(request) {
// Protect API routes
if (request.nextUrl.pathname.startsWith('/api/admin')) {
const token = await getToken({ req: request });
if (!token) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
);
}
if (token.role !== 'admin') {
return NextResponse.json(
{ error: 'Admin access required' },
{ status: 403 }
);
}
}
// Rate limiting (simplified)
const ip = request.ip || request.headers.get('x-forwarded-for');
if (ip) {
// Check rate limit in Redis
// ...
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*'
};
// API route with authentication
import { getServerSession } from 'next-auth';
export async function GET(request) {
const session = await getServerSession();
if (!session) {
return NextResponse.json(
{ error: 'Not authenticated' },
{ status: 401 }
);
}
// Fetch user-specific data
const data = await db.getUserData(session.user.id);
return NextResponse.json(data);
}
Common Mistakes
- Not handling HTTP methods correctly. API functions must handle GET, POST, PUT, DELETE separately. Return 405 Method Not Allowed for unsupported methods.
- No input validation. Never trust user input in API routes. Validate all fields, sanitize strings, and check types before processing.
- Exposing internal errors. Never return raw database errors, stack traces, or internal server details in API responses. Log them server-side and return generic error messages.
- No authentication on API routes. API routes are publicly accessible by default. Protect authenticated routes with session or token checks.
- Mixing SSR and API concerns. SSR page routes (pages/product.js) should not contain API logic. Keep API logic in pages/api/ routes for Separation Of Concerns.
Practice Questions
- How do API routes differ from SSR page routes in Next.js?
- How do you handle different HTTP methods in an API route?
- How do you secure an API route with authentication?
- What is the purpose of Cache-Control headers on API routes?
- How do you validate input in API routes?
Challenge: Build a complete CRUD API for a blog in Next.js: GET /api/posts (list), GET /api/posts/[id] (single), POST /api/posts (create), PUT /api/posts/[id] (update), DELETE /api/posts/[id] (delete). Include input validation, authentication middleware, proper HTTP status codes, error handling, and cache headers. Use the same data fetching function in both SSR pages and API routes.
FAQ
Mini Project
Build a full REST API for a blog within a Next.js application: API routes for posts, comments, and categories. All routes have input validation, authentication checks, proper error handling, and cache headers. The SSR pages use the same data functions as the API routes. Include a client-side JavaScript component that fetches from the API routes after hydration.
What's Next
You understand SSR API routes. Now explore SSR Middleware to handle authentication, logging, and request processing.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro