Creating a REST API in PHP with Slim framework

Developing a REST API is a basic skill in backend development. In this article, we'll walk through how to build a simple REST API to manage articles, step by step, using Slim, a very popular PHP micro-framework for creating lightweight and efficient REST APIs.

Why Slim framework?

Slim is a PHP micro-framework that makes handling HTTP routes, requests, responses, and middleware easy. With just a few lines of code, your API is up and running—without any heavy structure. It’s perfect for:

  • Quickly prototyping an API
  • Creating a lightweight backend
  • Maintaining full control

There are many other PHP micro-frameworks that can do the same thing—just pick the one that best fits your needs.

Prerequisites

Before getting started, make sure you have:

  • PHP ≥ 7.4
  • Composer installed

Composer really makes life easier for PHP developers by simplifying library installation 🙂

Installing Slim framework

With the following commands, we'll install Slim in a new folder called slim-api (I’m not great at naming folders, variables, functions… ChatGPT helps me a lot with that ^^):

mkdir slim-api && cd slim-api
composer require slim/slim:"^4.0" slim/psr7

This installs Slim 4 and a request/response handler (slim/psr7).

Now, let’s create the entry point: index.php

<?php
require __DIR__ . '/vendor/autoload.php';

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

$app = AppFactory::create();

// Example: GET route
$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $name = $args['name'];
    $response->getBody()->write("Hello, $name");
    return $response;
});

$app->run();

Start the server:

php -S localhost:8000

Open your browser: http://localhost:8000/hello/Cyril

Creating a REST API for articles

Now we'll create a mini CRUD for "articles", stored in a JSON file (Let's keep it simple: no database usage, just a simple file).

Sample data structure

In index.php, one function to save and another to retrieve articles from our data file. We initialize with 2 articles.

function getArticles() {
    if (!file_exists('articles.json')) {
        file_put_contents('articles.json', json_encode([
            1 => ["id" => 1, "titre" => "Article 1", "contenu" => "Contenu du premier article"],
            2 => ["id" => 2, "titre" => "Article 2", "contenu" => "Contenu du second article"]
        ]));
    }
    return json_decode(file_get_contents('articles.json'), true);
}

function saveArticles($articles) {
    file_put_contents('articles.json', json_encode($articles, JSON_PRETTY_PRINT));
}

$articles = getArticles();

Route GET /articles

The route to retrieve all articles.

$app->get('/articles', function (Request $request, Response $response) use ($articles) {
    $response->getBody()->write(json_encode(array_values($articles)));
    return $response->withHeader('Content-Type', 'application/json');
});

Route GET /articles/{id}

The route to retrieve a specific article.

$app->get('/articles/{id}', function (Request $request, Response $response, array $args) use ($articles) {
    $id = (int) $args['id'];
    if (isset($articles[$id])) {
        $article = $articles[$id];
        $response->getBody()->write(json_encode($article));
        return $response->withHeader('Content-Type', 'application/json');
    }
    $response->getBody()->write(json_encode(["message" => "Article not found."]));
    return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
});

Route POST /articles

The route to create a new article. For this route, we read the JSON data from the request:

$app->post('/articles', function (Request $request, Response $response) use (&$articles) {
    $article = json_decode($request->getBody()->getContents(), true);
    $article['id'] = end($articles)['id'] + 1;
    $articles[$article['id']] = $article;
    saveArticles($articles);
    $response->getBody()->write(json_encode($article));
    return $response->withHeader('Content-Type', 'application/json')->withStatus(201);
});

Route PUT /articles/{id}

The route to update an existing article.

$app->put('/articles/{id}', function (Request $request, Response $response, array $args) use (&$articles) {
    $id = (int) $args['id'];
    $article = json_decode($request->getBody()->getContents(), true);
    if (isset($articles[$id])) {
        $articles[$id] = array_merge($articles[$id], $article);
        saveArticles($articles);
        $response->getBody()->write(json_encode($articles[$id]));
        return $response->withHeader('Content-Type', 'application/json');
    }
    $response->getBody()->write(json_encode(["message" => "Article not found."]));
    return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
});

Route DELETE /articles/{id}

And finally, the route to delete an article.

$app->delete('/articles/{id}', function (Request $request, Response $response, array $args) use (&$articles) {
    $id = (int) $args['id'];
    if (isset($articles[$id])) {
        unset($articles[$id]);
        saveArticles($articles);
        return $response->withStatus(204);
    }
    $response->getBody()->write(json_encode(["message" => "Article not found."]));
    return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
});

All the code of the API

<?php
require __DIR__ . '/vendor/autoload.php';

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

function getArticles() {
    if (!file_exists('articles.json')) {
        file_put_contents('articles.json', json_encode([
            1 => ["id" => 1, "titre" => "Article 1", "contenu" => "Contenu du premier article"],
            2 => ["id" => 2, "titre" => "Article 2", "contenu" => "Contenu du second article"]
        ]));
    }
    return json_decode(file_get_contents('articles.json'), true);
}

function saveArticles($articles) {
    file_put_contents('articles.json', json_encode($articles, JSON_PRETTY_PRINT));
}

$app = AppFactory::create();

$app->get('/hello/{name}', function (Request $request, Response $response, array $args) {
    $name = $args['name'];
    $response->getBody()->write("Hello, $name");
    return $response;
});

$articles = getArticles();

$app->get('/articles', function (Request $request, Response $response) use ($articles) {
    $response->getBody()->write(json_encode(array_values($articles)));
    return $response->withHeader('Content-Type', 'application/json');
});

$app->get('/articles/{id}', function (Request $request, Response $response, array $args) use ($articles) {
    $id = (int) $args['id'];
    if (isset($articles[$id])) {
        $article = $articles[$id];
        $response->getBody()->write(json_encode($article));
        return $response->withHeader('Content-Type', 'application/json');
    }
    $response->getBody()->write(json_encode(["message" => "Article not found."]));
    return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
});

$app->post('/articles', function (Request $request, Response $response) use (&$articles) {
    $article = json_decode($request->getBody()->getContents(), true);
    $article['id'] = end($articles)['id'] + 1;
    $articles[$article['id']] = $article;
    saveArticles($articles);
    $response->getBody()->write(json_encode($article));
    return $response->withHeader('Content-Type', 'application/json')->withStatus(201);
});

$app->put('/articles/{id}', function (Request $request, Response $response, array $args) use (&$articles) {
    $id = (int) $args['id'];
    $article = json_decode($request->getBody()->getContents(), true);
    if (isset($articles[$id])) {
        $articles[$id] = array_merge($articles[$id], $article);
        saveArticles($articles);
        $response->getBody()->write(json_encode($articles[$id]));
        return $response->withHeader('Content-Type', 'application/json');
    }
    $response->getBody()->write(json_encode(["message" => "Article not found."]));
    return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
});

$app->delete('/articles/{id}', function (Request $request, Response $response, array $args) use (&$articles) {
    $id = (int) $args['id'];
    if (isset($articles[$id])) {
        unset($articles[$id]);
        saveArticles($articles);
        return $response->withStatus(204);
    }
    $response->getBody()->write(json_encode(["message" => "Article not found."]));
    return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
});

$app->run();

Testing the API

I usually use Postman, but a simple curl is enough to test the routes:

  • GET /articles → list all articles
  • GET /articles/1 → get a single article
  • POST /articles → create a new article (with {"titre": "New", "contenu": "Text"})
  • PUT /articles/1 → update an article
  • DELETE /articles/1 → delete an article

In REST, you can use other HTTP methods like PATCH. This article doesn’t aim to cover REST in depth—feel free to explore further on your own.

Benefits of Slim

This framework installs quickly via Composer and has a clean, readable syntax that makes it easy to get started. It's PSR-7 compliant, ensuring great interoperability with other PHP tools. Lightweight and flexible, it’s a great fit for microservices or headless APIs.

Conclusion

Slim framework is an excellent starting point for building REST APIs in PHP without diving into the complexity of a full-stack framework. It can be easily extended (middleware, DI container, etc.) while giving you full freedom in your architecture.

In just a few minutes, you can build a REST API with minimal effort—or even in seconds with AI. But I believe it’s important to take some time to understand how it works, so you can take ownership and easily fix bugs during maintenance...