Why I ditched MySQL for a JSON file — and built a lightning-fast CMS that runs on a $2/month hosting. ⚡
The Problem 💡
A client needed a simple blog. Requirements were basic:
- Easy content updates (no WordPress learning curve)
- Fast loading & minimal maintenance
Instead of a heavy MySQL setup, I went with JSON as a database. Result? A CMS that loads in under 100ms. 🎯
Project Structure 📁
JSON as Database 🗄️
{
"posts": [
{
"id": 1,
"title": "Why I Switched to CSS Grid",
"slug": "css-grid-why",
"excerpt": "Flexbox is great, but Grid solves problems...",
"content": "<p>For years I only used Flexbox...</p>",
"tags": ["css", "layout"],
"date": "2026-05-10",
"readTime": 5
}
]
}
Why JSON? ✅ Human-readable ✅ Git-friendly backups ✅ No migrations ✅ Works on any hosting
PHP API (No Framework!) 🔧
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
$jsonFile = __DIR__ . '/../data/posts.json';
$data = json_decode(file_get_contents($jsonFile), true);
$action = $_GET['action'] ?? 'list';
switch ($action) {
case 'list':
$posts = array_map(fn($p) => [
'id' => $p['id'], 'title' => $p['title'],
'slug' => $p['slug'], 'excerpt' => $p['excerpt'],
'date' => $p['date'], 'tags' => $p['tags'],
'readTime' => $p['readTime']
], $data['posts']);
usort($posts, fn($a, $b) =>
strtotime($b['date']) - strtotime($a['date']));
echo json_encode(['posts' => $posts]);
break;
case 'single':
$slug = $_GET['slug'] ?? '';
$post = array_values(array_filter($data['posts'],
fn($p) => $p['slug'] === $slug))[0] ?? null;
echo json_encode(['post' => $post]);
break;
}
💡 Smart bits: array_map for data projection (no full content in list view), usort for dynamic sorting, simple slug filtering.
Vanilla JS Frontend ⚡
const API_URL = '/blog/api/posts.php';
// Simple router 🗺️
const router = {
routes: {
'/': () => renderList(),
'/post/:slug': (slug) => renderPost(slug)
},
navigate(path) {
history.pushState({}, '', path);
this.handleRoute();
},
handleRoute() {
const path = location.pathname.replace('/blog', '') || '/';
if (path.startsWith('/post/')) {
this.routes['/post/:slug'](path.split('/post/')[1]);
return;
}
(this.routes[path] || this.routes['/'])();
}
};
// Fetch wrapper 📡
async function fetchJSON(url) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error('Fetch error:', err);
return null;
}
}
// Render post list 📋
async function renderList() {
const data = await fetchJSON(`${API_URL}?action=list`);
const app = document.getElementById('app');
if (!data?.posts) {
app.innerHTML = '<p>Error loading posts 😕</p>';
return;
}
app.innerHTML = `<div class="posts-grid">${data.posts.map(p => `
<article class="post-card">
<h2><a href="/blog/post/${p.slug}" data-nav>${p.title}</a></h2>
<div class="meta">📅 ${p.date} · ⏱️ ${p.readTime} min</div>
<p>${p.excerpt}</p>
<div>${p.tags.map(t => `<span class="tag">#${t}</span>`).join('')}</div>
</article>`).join('')}</div>`;
// Intercept clicks for SPA feel 🖱️
document.querySelectorAll('[data-nav]').forEach(link =>
link.addEventListener('click', e => {
e.preventDefault();
router.navigate(link.getAttribute('href'));
})
);
}
// Render single post 📝
async function renderPost(slug) {
const data = await fetchJSON(`${API_URL}?action=single&slug=${slug}`);
const app = document.getElementById('app');
if (!data?.post) {
app.innerHTML = '<p>Post not found 🔍</p>';
return;
}
const p = data.post;
app.innerHTML = `
<article class="single-post">
<a href="/blog/" data-nav>← Back</a>
<h1>${p.title}</h1>
<div class="meta">📅 ${p.date} · ⏱️ ${p.readTime} min</div>
<div>${p.tags.map(t => `<span class="tag">#${t}</span>`).join('')}</div>
<div class="content">${p.content}</div>
</article>`;
// Re-attach click handlers
document.querySelectorAll('[data-nav]').forEach(link =>
link.addEventListener('click', e => {
e.preventDefault();
router.navigate(link.getAttribute('href'));
})
);
}
// Init 🚀
window.addEventListener('popstate', () => router.handleRoute());
document.addEventListener('DOMContentLoaded', () => router.handleRoute());
💡 Why vanilla JS? No 200KB framework. Custom router gives SPA feel. Progressive enhancement ready.
CSS — Clean & Light 🎨
/* Light theme, dev-friendly 🌤️ */
:root {
--bg: #f1f5f7;
--surface: #ffffff;
--text: #233742;
--accent: #76a5af;
--border: #b4cddd;
}
.posts-grid {
display: grid;
gap: 1.5rem;
}
.post-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.5rem;
transition: transform 0.2s;
}
.post-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.tag {
background: var(--bg);
color: var(--accent);
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
}
✅ Pros vs ❌ Cons 🎨
✅ Advantages
- ⚡ No DB queries = <100ms response
- 🛡️ No SQL injection possible
- 💾 Git commit = DB backup
- 💰 Runs on any $2 hosting
- 🧠 You understand every part
❌ Limitations
- 👥 No multi-author support
- 📊 1000+ posts = slow JSON
- 🔗 No complex relations
- 🔒 No built-in auth
- 📝 Manual content editing
When to Use / Avoid 🎯
Use it for: ✅ Personal blogs ✅ Portfolio sites ✅ Small client projects ✅ Prototyping ✅ Learning
Avoid for: ❌ E-commerce ❌ User-generated content ❌ Multi-user platforms ❌ Complex data relationships
Takeaway 🏆
"The right" tech isn't always the "best" tech. For small projects, simplicity wins. PHP + JSON + vanilla JS = powerful enough for 90% of use cases I encounter. 🚀
🎮 Your Challenge
Try adding these features:
- 🔍 Client-side search (
posts.filter()) - 📄 Pagination (
array_slice()in PHP) - ✏️ Admin panel (
file_put_contents()) - 🌙 Light/dark theme toggle
What do you think? 💬
Would you use JSON as a database? Or is this too "hacky"? 🤔
Drop your thoughts below! 👇
📌 Follow me for more no-BS web dev tips!
Dervic is a web developer who believes in keeping things simple. When not coding, he's probably optimizing something that doesn't need optimizing.
Built with HTML, CSS, JS & a lot of ☕. No frameworks were harmed in the making of this blog.
If you liked this post, please LIKE or COMMENT below! 💬✨
Comments (0)