Building a Lightweight JSON API with PHP
My blog needed an API for infinite scroll and admin functions. Laravel felt like overkill, so I built a micro-router in pure PHP.
The Router
<?php
// api/index.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path = str_replace('/api', '', $path);
$routes = [
'GET' => [
'/posts' => 'getPosts',
'/posts/{slug}' => 'getPost',
],
'POST' => [
'/posts' => 'createPost',
'/contact' => 'handleContact'
]
];
function respond($data, $code = 200) {
http_response_code($code);
echo json_encode($data, JSON_PRETTY_PRINT);
exit;
}
// Simple route matching
foreach ($routes[$method] ?? [] as $route => $handler) {
$pattern = preg_replace('/\{([a-z]+)\}/', '([a-z0-9-]+)', $route);
if (preg_match("#^$pattern$#", $path, $matches)) {
array_shift($matches); // Remove full match
call_user_func_array($handler, $matches);
exit;
}
}
respond(['error' => 'Not found'], 404);
The Handlers
<?php
function getPosts() {
$store = new JsonStore('posts.json');
$posts = array_filter($store->all(), fn($p) => $p['published']);
// Pagination
$page = (int)($_GET['page'] ?? 1);
$perPage = 6;
$offset = ($page - 1) * $perPage;
$posts = array_slice(array_values($posts), $offset, $perPage);
respond([
'data' => $posts,
'meta' => [
'page' => $page,
'total' => count($store->all())
]
]);
}
function getPost($slug) {
$store = new JsonStore('posts.json');
$post = $store->findBySlug($slug);
if (!$post) {
respond(['error' => 'Post not found'], 404);
}
// Increment views
$posts = $store->all();
$key = array_search($slug, array_column($posts, 'slug'));
$posts[$key]['views']++;
$store->save($posts);
respond(['data' => $post]);
}
function createPost() {
// Auth check
session_start();
if (empty($_SESSION['admin'])) {
respond(['error' => 'Unauthorized'], 401);
}
$input = json_decode(file_get_contents('php://input'), true);
// Validation
if (empty($input['title']) || empty($input['content'])) {
respond(['error' => 'Title and content required'], 422);
}
$store = new JsonStore('posts.json');
$posts = $store->all();
$newPost = [
'id' => max(array_column($posts, 'id')) + 1,
'slug' => strtolower(preg_replace('/[^a-z0-9]+/i', '-', $input['title'])),
'title' => htmlspecialchars($input['title']),
'date' => date('Y-m-d'),
'published' => false,
'views' => 0,
...$input
];
$posts[] = $newPost;
$store->save($posts);
respond(['data' => $newPost], 201);
}
JavaScript Consumption
// Infinite scroll implementation
const observer = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting && !loading) {
loading = true;
const { data, meta } = await fetchJSON(`/api/posts?page=${page}`);
data.forEach(post => {
const card = document.createElement('blog-card');
card.dataset.title = post.title;
card.dataset.excerpt = post.excerpt;
grid.appendChild(card);
});
if (data.length === 6) page++;
loading = false;
}
});
observer.observe(document.querySelector('#load-more-trigger'));
Why Not Laravel?
For 6 routes and 1 JSON file? Laravel adds:
- 50MB of vendor files
- Composer complexity
- Slower boot times
My entire API is 3 files, 8KB total.
The best framework is the one you understand deeply. Sometimes that's no framework.