91 Case Study: Article Management System (CMS)
91 Case Study: Article Management System (CMS)
A CMS (Content Management System) is one of the most fundamental systems in the modern software ecosystem. In today’s fully digital era, the need to manage content in a structured, scalable, and secure way is critical—whether for a personal blog, a news portal, or an enterprise knowledge base.
This article is part of the 91 Case Studies series, in which we dissect the architecture, implementation, and best practices of building a CMS using an engineering-first approach, while also presenting a case study and pragmatic implementation code examples.
1. Problem & Requirements
Case Study: The “MediaKita” News Portal
Scenario:
MediaKita is a growing news portal. Initially, articles were uploaded manually by the team via Google Docs, then copy-pasted into the website by developers. This process was not scalable and frequently caused problems, such as:
- Formatting and metadata errors.
- Duplicate articles.
- Frequently missed deadlines.
- No approval/editorial workflow system.
Key Requirements from stakeholders:
- Every user (author) can write, save, and publish articles.
- Articles must be categorizable, taggable, and have a status (draft, review, published).
- Editors can review and approve articles before they are published.
- The system must be trackable (audit trail).
- A REST API for frontend & mobile app integration.
2. High Level Design
Let’s break this down into several components:
flowchart TD
subgraph Frontend
FE[React/Vue SPA]
end
subgraph Backend
BE[REST API (Express/NestJS)]
end
subgraph Database
DB[(PostgreSQL/MongoDB)]
end
FE -- HTTP/JSON --> BE
BE -- ORM/ODM --> DB
Entity Model
The following table illustrates the 3 core entities in a simple CMS system.
| Entity | Fields | Relationship |
|---|---|---|
| User | id, name, email, role (author/editor/admin), password_hash | 1 User : n Article |
| Article | id, title, slug, content, category_id, status, created_at, updated_at, author_id, editor_id, audit_log | n Article : 1 Category |
| Category | id, name, slug, parent_id | n Category : n Article |
User Stories
1. Author Writes an Article
- The user opens the form page, fills in the title and content, and selects a category.
- Saves it as a draft.
2. Editorial Workflow
- The draft enters the review queue.
- The editor provides feedback or approves it.
- Once approved, the article status changes to
published.
3. Audit Trail
- Every status or content change is recorded in the audit log.
3. Code Example: Backend API (Node.js + Express + Sequelize)
Let’s see how the context above translates into RESTful endpoints:
1// src/routes/articles.js
2const express = require('express');
3const router = express.Router();
4const { Article, Category, User } = require('../models');
5const auth = require('../middleware/auth');
6
7// Endpoint: Create an article (Draft)
8router.post('/', auth, async (req, res) => {
9 try {
10 const { title, content, category_id } = req.body;
11 const article = await Article.create({
12 title, content, category_id,
13 author_id: req.user.id,
14 status: 'draft'
15 });
16 res.status(201).json(article);
17 } catch (error) {
18 res.status(400).json({ error: error.message });
19 }
20});
21
22// Endpoint: Update article status to 'review'
23router.post('/:id/submit', auth, async (req, res) => {
24 try {
25 const article = await Article.findByPk(req.params.id);
26 if (article.author_id !== req.user.id) return res.status(403).json({ error: "Unauthorized" });
27 article.status = 'review';
28 await article.save();
29 res.json(article);
30 } catch (error) {
31 res.status(400).json({ error: error.message });
32 }
33});
34
35// Endpoint: Approve an article (Editor)
36router.post('/:id/approve', auth, async (req, res) => {
37 try {
38 if (req.user.role !== 'editor') return res.status(403).json({ error: "Editor only" });
39 const article = await Article.findByPk(req.params.id);
40 article.status = 'published';
41 article.editor_id = req.user.id;
42 await article.save();
43 res.json(article);
44 } catch (error) {
45 res.status(400).json({ error: error.message });
46 }
47});4. Editorial Workflow: State Machine
To keep the editorial process robust, the status workflow is usually managed via a state machine.
stateDiagram-v2
[*] --> Draft
Draft --> Review: Submit for Review
Review --> Draft: Request Change
Review --> Published: Approve
Published --> Archived: Archive
Draft --> Deleted: Delete
Review --> Deleted: Delete
5. Audit Trail: Simulation & Implementation
An audit trail is important for governance and debugging. Here’s a simple simulation using a hook on the ORM model:
1// src/models/Article.js (Sequelize Model)
2Article.afterUpdate(async (article, options) => {
3 await AuditLog.create({
4 article_id: article.id,
5 changed_by: options.user.id,
6 status: article.status,
7 timestamp: new Date()
8 });
9});Every time an article’s status changes, a record is inserted into the audit_logs table.
Audit Log Query Example
1SELECT * FROM audit_logs WHERE article_id = 123 ORDER BY timestamp DESC;6. Categories and Tagging
Organizing content is essential for navigation and discoverability. For example, a single article can have many tags.
Many-to-Many Relationship Table:
| article_id | tag_id |
|---|---|
| 10 | 3 |
| 10 | 8 |
| 12 | 3 |
7. Frontend Integration
With a REST API in place, the frontend can be built with modern frameworks such as React.
Example Article Fetch Simulation:
1useEffect(() => {
2 fetch("/api/articles?status=published")
3 .then(res => res.json())
4 .then(data => setArticles(data));
5}, []);8. Deployment & Scalability
- Database: PostgreSQL, with full-text search as an option for article search.
- Backend: Node.js/NestJS/Go, with stateless services recommended for easier scaling.
- Frontend: SPA, caching important data on a CDN when traffic is high.
- OAuth Integration: For secure login.
9. Live Case Study: Workflow Simulation
Editorial Flow:
- The author creates a
draftarticle. - Submits it for review.
- The editor provides feedback (
request change) or approves it. - If approved, the article is published automatically.
API Request Simulation:
1# 1. Author create
2curl -X POST /api/articles -d '{ "title": "CMS Rules", "content": "...", ... }'
3
4# 2. Author submit
5curl -X POST /api/articles/5/submit
6
7# 3. Editor approve
8curl -X POST /api/articles/5/approveSample Audit Trail Table:
| ID | Article ID | Changed By | Status | Time |
|---|---|---|---|---|
| 1 | 5 | 12 | draft | 2024-06-01 09:01:01 |
| 2 | 5 | 12 | review | 2024-06-01 09:02:00 |
| 3 | 5 | 13 | published | 2024-06-01 11:10:00 |
10. Lessons Learned & Best Practices
- Separation of Concerns: Strictly separate the model, controller, and service layers.
- Access Control: Implement RBAC (Role-Based Access Control) on both endpoints and the UI.
- State Machine: Don’t hardcode statuses as magic strings; use the state machine pattern instead.
- Audit Trail: With a change log, we can easily trace who changed what.
- API-First: Design the backend so it can be easily integrated with frontend/web/mobile.
- Validation & Error Handling: Don’t forget the validation layer on both the backend and frontend.
11. Conclusion
Building a CMS is not just about CRUD operations on articles—it’s about designing a workflow system, ensuring data integrity, and enabling easy, enterprise-ready scaling. From the MediaKita case study, editorial needs that once relied on manual processes can now be automated and properly monitored. Its main value lies in flexibility, governance, and extensibility.
We hope this case study breakdown helps those of you who are designing or refactoring a CMS to scale up to the next level. Don’t hesitate to experiment, iterate, and always audit every critical flow in your system!
What has your experience been like building a CMS? Share it in the comments!