Projects About me Blog FAQ Contact me

Building a Lightweight JSON API with PHP (No Framework)

How I built a RESTful API endpoint in 50 lines of PHP to serve my blog content to JavaScript fetch calls.

June 12, 2026 · BLOG
Building a Lightweight JSON API with PHP (No Framework)

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.

More from the blog