Lesson 35 of 5032 min read

Next.js Full-Stack Project: Blog CMS with Prisma and Auth.js

Bring together authentication, database-backed CRUD, and admin workflows into one complete, full-stack Blog CMS built with Next.js, Prisma, and Auth.js.

Author: CodersNexus

Next.js Full-Stack Project: Blog CMS with Prisma and Auth.js

This lesson is the capstone of Module 5, bringing together nearly every concept covered across this course into one cohesive, realistic application: a Blog CMS (Content Management System) where authenticated admins can create, edit, publish, and delete blog posts, while the public can read published content through fast, well-optimized pages built on everything covered in earlier modules.

Rather than introducing new concepts, this lesson focuses on architecture and integration — how database schema design (Lesson 5.2), authentication (5.4), authorization (5.5), a REST API or Server Actions (5.3/4.3), rendering strategy (Module 2), and UI patterns (Module 3) all fit together into one complete, production-shaped application, and the specific decisions involved in wiring them together correctly.

Learning Objectives

  • Design a Prisma schema supporting a blog's core entities: posts, authors, and publication status.
  • Implement authenticated, role-protected admin routes for creating and managing posts.
  • Combine Server Actions for admin mutations with public, statically/ISR-rendered pages for readers.
  • Apply RBAC so only authenticated admins can access content-management functionality.
  • Architect a cohesive full-stack application integrating every major concept from this course.

Core Definitions

  • CMS (Content Management System): An application allowing authorized users to create, edit, organize, and publish content, typically separate from the public-facing presentation of that content.
  • Admin workflow: The sequence of authenticated, authorized actions a content manager takes to create, review, and publish content within a CMS.
  • Public-facing pages: The pages of an application, like published blog posts, intended to be viewed by any visitor, regardless of authentication status.
  • Draft vs published status: A common content model distinction where new content starts as a draft (visible only to its author/admins) and becomes publicly visible only once explicitly published.
  • Full-stack integration: The practice of combining frontend rendering, backend data access, authentication, and authorization into one cohesive, correctly interacting application.

Detailed Explanation

The Blog CMS's data model, defined in Prisma (Lesson 5.2), centers on a Post model with a status field distinguishing draft from published content, and an author relation linking each post back to a User (which itself carries a role field from Lesson 5.5's RBAC pattern, distinguishing regular users from admins who can manage content). This single schema decision — separating draft from published status — is what allows the entire application to cleanly separate two very different experiences from the same underlying data: an admin-only management interface showing all posts regardless of status, and a public-facing set of pages showing only published posts.

The admin side of the application lives under an authenticated, role-protected route section (say, /admin), enforced by the middleware-based RBAC pattern from Lesson 5.5 — only users with the admin role can reach these routes at all. Within this admin section, creating and editing posts uses Server Actions (Lesson 4.3), validated with a shared Zod schema (Lesson 3.4) before touching the database via Prisma, and calling revalidatePath or revalidateTag (Lesson 2.4) after a successful mutation to ensure the public-facing pages correctly reflect changes without requiring a manual redeploy.

The public-facing side of the application — the actual blog posts readers see — takes full advantage of the rendering strategies from Module 2: a blog listing page and individual post pages can be built with generateStaticParams (Lesson 2.2) to pre-render every currently published post at build time, combined with a revalidate window or on-demand revalidation (Lesson 2.4) so that newly published or edited posts appear promptly without needing a full site rebuild. Crucially, the query fetching posts for these public pages must always explicitly filter by `where: { status: 'published' }` — never showing draft content to public visitors — a detail easy to overlook but critical for correctly separating the two audiences this single application serves.

Beyond the core CRUD functionality, a genuinely complete admin workflow benefits from the UI and UX patterns covered throughout the course: reusable form components (Lesson 3.3) for the post-editing interface, proper loading and error states (Lessons 2.6/4.4) while posts save or load, and SEO metadata (Lesson 3.7) — including generateMetadata computing each individual published post's title, description, and Open Graph tags from its actual content — applied consistently across every public-facing post page.

The genuine skill this capstone project builds isn't any single new technique, but rather architectural judgment: correctly deciding which rendering strategy fits which specific page (admin pages, needing fresh, per-request data, naturally lean toward SSR; public post pages, changing relatively infrequently, naturally lean toward SSG combined with revalidation), and ensuring authentication, authorization, validation, and data-fetching concerns are each applied at exactly the right layer, consistent with how each was introduced individually throughout the course.

Blog CMS Architecture: How Every Module Concept Fits Together

{"heading":"Blog CMS Architecture: How Every Module Concept Fits Together","description":"Visualize how the full-stack pieces connect:\n\nDATA LAYER (Prisma, Lesson 5.2):\n User { role: 'admin' | 'user' } ---< Post { status: 'draft' | 'published' }\n\nADMIN SIDE (protected by RBAC, Lesson 5.5):\n /admin/posts (list ALL posts, any status)\n /admin/posts/new --> Server Action: createPost() --> validates (Zod) --> Prisma create --> revalidatePath('/blog')\n /admin/posts/[id]/edit --> Server Action: updatePost() --> same validation + revalidation pattern\n\nPUBLIC SIDE (SSG + ISR, Module 2):\n /blog --> generateStaticParams + revalidate --> only status: 'published' posts\n /blog/[slug] --> generateMetadata (Lesson 3.7) --> SEO + Open Graph per post\n\nA single Prisma schema and database power BOTH sides, filtered and protected differently for each audience."}

Next.js Practical Example

// prisma/schema.prisma — core Blog CMS data model
model User {
  id    String @id @default(cuid())
  email String @unique
  role  String @default("user") // 'user' | 'admin'
  posts Post[]
}

model Post {
  id        String   @id @default(cuid())
  title     String
  slug      String   @unique
  content   String
  status    String   @default("draft") // 'draft' | 'published'
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  updatedAt DateTime @updatedAt
}

// app/actions/posts.ts — admin-only Server Action, tying together validation + auth + revalidation
'use server';
import { auth } from '@/auth';
import { prisma } from '@/lib/prisma';
import { postSchema } from '@/schemas/postSchema';
import { revalidatePath } from 'next/cache';

export async function publishPost(postId: string) {
  const session = await auth();
  if (session?.user.role !== 'admin') throw new Error('Not authorized');

  await prisma.post.update({
    where: { id: postId },
    data: { status: 'published' },
  });

  revalidatePath('/blog'); // public listing reflects the new post immediately
}

// app/blog/page.tsx — public listing, SSG + ISR, published posts ONLY
export const revalidate = 3600;

export default async function BlogListingPage() {
  const posts = await prisma.post.findMany({
    where: { status: 'published' }, // critical: never expose drafts publicly
    orderBy: { updatedAt: 'desc' },
  });

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}><a href={`/blog/${post.slug}`}>{post.title}</a></li>
      ))}
    </ul>
  );
}

The Prisma schema's status field on Post is the single design decision enabling the entire dual-audience architecture: it's what lets the exact same posts table serve both an admin's complete view and the public's filtered, published-only view. publishPost demonstrates the full integration this lesson is about: it checks the session's role (RBAC, Lesson 5.5) before doing anything, performs the actual database update via Prisma (Lesson 5.2), and calls revalidatePath immediately afterward (Lesson 2.4) so the public blog listing reflects the newly published post without requiring a full site rebuild. BlogListingPage shows the public side: a revalidate window gives it ISR behavior, and the explicit `where: { status: 'published' }` filter is the critical detail ensuring draft content, however it might exist in the same database table, never becomes visible to public visitors.

Real-World Products Built on This Same Architecture

  • Company blogs and marketing sites frequently use exactly this architecture — an internal, authenticated admin panel for content teams, paired with fast, publicly-cached blog pages for readers, all backed by one shared database.
  • Documentation platforms with an editing workflow (draft, review, publish) apply the same draft/published status pattern, ensuring in-progress documentation edits aren't visible to the public until explicitly published.
  • Newsrooms and media publishers rely on this same separation between an authenticated editorial/admin workflow and fast, SEO-optimized, publicly cached article pages, often with additional intermediate states like 'in review' beyond just draft/published.
  • SaaS products with a 'changelog' or 'release notes' section commonly build a small, similarly structured internal CMS, letting the product team publish updates without needing a developer to manually deploy new content.
  • Educational platforms and course providers (conceptually similar to this very course's own lesson content) often maintain an internal admin interface for authoring and publishing lessons, paired with a fast, publicly accessible course-browsing experience for learners.

Common Mistakes to Avoid

  • Forgetting the explicit status: 'published' filter on public-facing queries, accidentally exposing draft content to visitors.
  • Using the same rendering strategy (e.g., pure SSG with no revalidation) for both admin and public pages, when their freshness needs are genuinely different.
  • Checking authorization only in middleware and skipping the redundant, resource-specific check inside Server Actions, missing the defense-in-depth principle from Lesson 5.5.
  • Forgetting to call revalidatePath (or revalidateTag) after an admin mutation, leaving public pages showing stale content until the next scheduled revalidation or full rebuild.
  • Not reusing the shared Zod validation schema between the admin post-editing form and its corresponding Server Action, risking inconsistent validation rules.

Interview Notes

  • A status field (draft/published) on the content model is the key design decision enabling a single data source to serve both admin and public audiences correctly.
  • Admin routes typically use dynamic (SSR) rendering for freshness; public content pages typically use SSG combined with ISR for speed.
  • Public-facing queries must always explicitly filter by published status to avoid exposing draft content.
  • revalidatePath/revalidateTag, called after an admin mutation, keeps statically generated public pages in sync without a full rebuild.
  • Defense in depth (middleware + in-action authorization checks) protects admin functionality redundantly, consistent with Lesson 5.5's RBAC principles.

Key Takeaways

  • This capstone project isn't about learning anything brand new — it's about correctly integrating everything from database design through rendering strategy into one coherent application.
  • The draft/published status field is a small schema decision with an outsized architectural impact, cleanly separating two very different audiences from one data source.
  • Matching rendering strategy to each section's actual freshness needs (SSR for admin, SSG+ISR for public content) reflects the deliberate, informed decision-making Module 2 aimed to build.
  • Defense in depth, shared validation schemas, and consistent revalidation after mutations are the specific, recurring patterns that turn individually-learned concepts into a genuinely production-quality application.

Summary

This capstone Blog CMS project integrates nearly every concept from the course into one complete, full-stack application. A Prisma-modeled Post entity with a draft/published status field, related to a User carrying an RBAC role, is the foundational design decision enabling the same underlying data to serve two distinct audiences: an authenticated, role-protected admin section (built with Server Actions, shared Zod validation, and dynamic SSR rendering suited to always-fresh management data) and a fast, public-facing set of blog pages (built with SSG and ISR, explicitly filtered to only published content, with per-post SEO metadata via generateMetadata). Admin mutations call revalidatePath after successfully changing data, ensuring the statically generated public pages stay in sync without requiring a full site rebuild. Authorization is enforced with defense in depth — both broadly via middleware and specifically inside each Server Action — reflecting the RBAC principles from earlier in this module. The genuine skill this project builds is architectural: correctly matching each piece of the application (data modeling, authentication, authorization, rendering strategy, validation, and revalidation) to its appropriate layer, exactly as each was introduced individually throughout the course.