Back to Blog
Software Development

Next.js + Django REST: The Full-Stack Setup We Rely On

A deep-dive into the monorepo structure, auth patterns, and deployment pipeline we use across most of our client projects.

Adsesugh Igba
May 20, 2025
10 min read

Why This Stack?

Choosing a technology stack is one of the most consequential decisions in a project's lifecycle. After evaluating dozens of combinations, we've settled on Next.js for the frontend and Django REST Framework for the backend. This isn't a trendy choice — it's a pragmatic one born from shipping production applications.

The Rationale

  • Next.js gives us SSR, SSG, API routes, and an incredible developer experience with TypeScript
  • Django REST Framework provides battle-tested ORM, admin interface, authentication, and serialization out of the box
  • Together, they cover 90% of use cases without reaching for additional tools

Monorepo Structure

We organize projects in a monorepo using a clean separation between frontend and backend:

project-root/
├── apps/
│   ├── web/                 # Next.js application
│   │   ├── src/
│   │   │   ├── app/         # App Router pages
│   │   │   ├── components/  # React components
│   │   │   ├── lib/         # Utilities & API client
│   │   │   └── types/       # TypeScript interfaces
│   │   ├── next.config.ts
│   │   └── package.json
│   └── api/                 # Django application
│       ├── core/            # Django project settings
│       ├── apps/            # Django apps (users, orders, etc.)
│       ├── requirements/
│       └── manage.py
├── packages/
│   └── shared/              # Shared types, constants
├── docker-compose.yml
├── Makefile
└── turbo.json               # Turborepo config

This structure gives us independent deployability while sharing types and constants across the stack.

Authentication Pattern

Authentication is where most full-stack setups get complex. Our approach uses HTTP-only cookies for session management with JWT tokens for API authentication:

// lib/api-client.ts — Frontend API client with automatic token refresh
import { cookies } from "next/headers";

class APIClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
    const cookieStore = await cookies();
    const token = cookieStore.get("access_token")?.value;

    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        "Content-Type": "application/json",
        ...(token && { Authorization: `Bearer ${token}` }),
        ...options.headers,
      },
    });

    if (response.status === 401) {
      // Attempt token refresh
      const refreshed = await this.refreshToken();
      if (refreshed) return this.request<T>(endpoint, options);
      throw new AuthError("Session expired");
    }

    if (!response.ok) {
      throw new APIError(response.status, await response.json());
    }

    return response.json();
  }

  private async refreshToken(): Promise<boolean> {
    // Implementation details...
    return true;
  }
}

Django Side — Token Configuration

# core/settings.py
from datetime import timedelta

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True,
    "AUTH_HEADER_TYPES": ("Bearer",),
}

# Cookie settings for cross-origin setup
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"

Deployment Pipeline

Our CI/CD pipeline uses GitHub Actions with separate workflows for frontend and backend:

  1. On PR — Lint, type-check, run unit tests, build Docker images
  2. On merge to main — Deploy to staging automatically
  3. On tag — Deploy to production with manual approval gate
# .github/workflows/deploy.yml (simplified)
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy-api:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build & push API image
        run: |
          docker build -t registry/api:${{ github.sha }} ./apps/api
          docker push registry/api:${{ github.sha }}
      - name: Deploy to staging
        run: kubectl set image deployment/api api=registry/api:${{ github.sha }}

  deploy-web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}

Database Patterns

We use PostgreSQL consistently, with Django migrations managing the schema:

  • Multi-tenant isolation via schema separation for SaaS products
  • Read replicas for heavy query workloads
  • Connection pooling with PgBouncer for high-concurrency scenarios

Performance Optimizations

Frontend

  • ISR (Incremental Static Regeneration) for content pages
  • React Server Components for data-heavy pages — reduces client JS bundle
  • Image optimization via Next.js <Image> with CDN-backed loader

Backend

  • Select-related / prefetch-related queries to eliminate N+1 problems
  • Redis caching for frequently accessed data (user profiles, feature flags)
  • Celery for background task processing (emails, report generation)

Lessons Learned

  1. Type safety across the stack saves hours of debugging. Generate TypeScript types from Django serializers using tools like django-typer or OpenAPI schemas.

  2. Invest in developer experience early. A good Makefile, Docker Compose setup, and seed data scripts pay dividends throughout the project lifecycle.

  3. Don't over-engineer authentication. HTTP-only cookies with short-lived JWTs handle 95% of cases. Only reach for OAuth if you truly need third-party integration.

  4. Monitor everything from day one. Structured logging, error tracking (Sentry), and basic APM (application performance monitoring) should be in your MVP.

This stack has served us well across healthtech platforms, fintech dashboards, and e-commerce applications. It's not the only way to build full-stack applications, but it's a reliable foundation that scales predictably.

Ready to build something extraordinary?

We help startups and enterprises ship production-grade software powered by AI. Let's discuss your next project.

Get In Touch
    Next.js + Django REST: The Full-Stack Setup We Rely On — Sughware Blog | Sughware Technologies