Building a Modern API Server with Slim 4 and WordPress

Building a Modern API Server with Slim 4 and WordPress

Codeable.io

In this comprehensive tutorial, I’ll show you how to build a modern, production-ready REST API server using Slim Framework 4 with WordPress authentication. We’ll leverage PHP 8.1+ features, implement modern security practices, and follow current best practices for API development in 2025.

Why This Matters in 2025

As WordPress continues to power over 40% of the web, the need to expose WordPress data through custom APIs remains crucial. While WordPress REST API is powerful, sometimes you need a custom API with:

  • Custom authentication logic
  • Specific performance optimizations
  • Integration with external services
  • Lightweight, focused endpoints

This tutorial will show you how to build exactly that using Slim Framework 4, which is fast, secure, and follows PSR standards.

What We’ll Build

We’re building a REST API that:

  • Authenticates users against WordPress credentials
  • Provides multiple endpoints (health check, user info, posts)
  • Implements CORS for modern frontend frameworks
  • Includes structured logging with Monolog
  • Uses environment-based configuration
  • Follows PHP 8.1+ best practices with strict typing

Prerequisites

Before we start, ensure you have:

  • PHP 8.1 or higher
  • Composer 2.x
  • A WordPress installation (5.0+)
  • Basic understanding of REST APIs
  • Apache or Nginx with URL rewriting

The complete working repository is available at: https://github.com/krasenslavov/api-server-slim-wordpress

Let’s dive in!

Step 1: Project Setup

First, create an api/ directory at the top level of your WordPress installation. This keeps your API code separate from WordPress core files.

cd /path/to/wordpress
mkdir api
cd api

Setting Up Composer

Create a composer.json file with modern dependencies:

{
  "name": "api-server-slim-wordpress",
  "description": "API server using Slim Framework 4 with WordPress authentication",
  "type": "project",
  "license": "MIT",
  "require": {
    "php": "^8.1",
    "slim/slim": "^4.14",
    "slim/psr7": "^1.7",
    "tuupola/slim-basic-auth": "^3.3",
    "vlucas/phpdotenv": "^5.6",
    "monolog/monolog": "^3.7"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}

Key updates from 2022 to 2025:

  • PHP 8.1+ requirement for modern features (union types, enums, readonly properties)
  • Slim 4 instead of Slim 3 (PSR-7, PSR-15 middleware)
  • PSR-7 implementation (slim/psr7) – now separate from Slim
  • Updated authentication middleware (v3.3)
  • DotEnv for environment configuration
  • Monolog for professional logging

Install dependencies:

composer install

Step 2: Project Structure

Create the following structure:

wordpress/
├── api/
│   ├── vendor/           (generated by Composer)
│   ├── logs/            (create this manually)
│   ├── .env             (configuration)
│   ├── .env.example     (template)
│   ├── .htaccess        (Apache rewrite rules)
│   ├── .gitignore       (exclude sensitive files)
│   ├── composer.json
│   ├── composer.lock    (generated by Composer)
│   └── index.php        (main API file)
├── wp-load.php
└── ... (other WordPress files)

Create the logs directory:

mkdir logs
chmod 755 logs

Apache Configuration (.htaccess)

Create .htaccess for clean URLs:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

Nginx Configuration

If using Nginx, add this to your site config:

location /api {
    try_files $uri $uri/ /api/index.php?$query_string;
}

Environment Configuration

Create .env.example:

# WordPress Configuration
WP_LOAD_PATH=../wp-load.php

# Authentication
AUTH_REALM=Protected API
AUTH_PATH=/
AUTH_SECURE=true
WP_AUTH_ROLES=subscriber

# CORS Configuration
CORS_ORIGIN=*

# Debug Mode
DEBUG=false

Copy to .env and customize:

cp .env.example .env

Git Ignore

Create .gitignore:

/vendor/
/logs/
.env
composer.lock
*.log

Step 3: The Code

Now for the main index.php file. Let’s break it down section by section.

Bootstrap and Dependencies

<?php

declare(strict_types=1);

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Tuupola\Middleware\HttpBasicAuthentication;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

// Autoloader for Composer packages
require_once __DIR__ . '/vendor/autoload.php';

// Load environment variables
if (file_exists(__DIR__ . '/.env')) {
    $dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
    $dotenv->load();
}

// Load WordPress
$wpLoadPath = $_ENV['WP_LOAD_PATH'] ?? __DIR__ . '/../wp-load.php';
if (!file_exists($wpLoadPath)) {
    die('WordPress not found. Please configure WP_LOAD_PATH in .env');
}
require_once $wpLoadPath;

Modern improvements:

  • declare(strict_types=1) – Enforces type safety
  • PSR-7 imports for request/response handling
  • Environment-based WordPress loading with fallback
  • Proper error handling for missing WordPress

Logging Setup

// Set up logging
$logger = new Logger('api');
$logger->pushHandler(new StreamHandler(__DIR__ . '/logs/app.log', Logger::INFO));

Monolog provides structured logging for debugging and monitoring in production.

Create the Slim Application

// Create Slim app
$app = AppFactory::create();

// Add error middleware
$errorMiddleware = $app->addErrorMiddleware(
    $_ENV['DEBUG'] ?? false,
    true,
    true,
    $logger
);

Key difference from Slim 3: In Slim 4, you create the app using AppFactory::create() instead of new \Slim\App().

CORS Middleware

// Add CORS middleware
$app->add(function (Request $request, $handler) {
    $response = $handler->handle($request);
    return $response
        ->withHeader('Access-Control-Allow-Origin', $_ENV['CORS_ORIGIN'] ?? '*')
        ->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
        ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
});

This enables your API to be consumed by modern JavaScript frameworks like React, Vue, or Angular.

WordPress User Authentication

// Helper function to get authenticated WordPress users
function getAuthenticatedUsers(): array
{
    static $users = null;

    if ($users === null) {
        $users = [];
        $allowedRoles = explode(',', $_ENV['WP_AUTH_ROLES'] ?? 'subscriber');
        $wpUsers = get_users(['role__in' => $allowedRoles]);

        foreach ($wpUsers as $user) {
            $users[$user->user_login] = [
                'password' => $user->user_pass,
                'display_name' => $user->display_name,
                'email' => $user->user_email,
                'id' => $user->ID
            ];
        }
    }

    return $users;
}

Improvements:

  • Static caching prevents multiple database queries
  • Configurable user roles via environment
  • Includes more user data for richer responses

HTTP Basic Authentication Middleware

// Basic HTTP Authentication middleware
$app->add(new HttpBasicAuthentication([
    'realm' => $_ENV['AUTH_REALM'] ?? 'Protected API',
    'path' => $_ENV['AUTH_PATH'] ?? '/',
    'secure' => filter_var($_ENV['AUTH_SECURE'] ?? true, FILTER_VALIDATE_BOOLEAN),
    'relaxed' => ['localhost', '127.0.0.1'],
    'authenticator' => function (array $arguments) use ($logger): bool {
        $users = getAuthenticatedUsers();
        $username = $arguments['user'] ?? '';
        $password = $arguments['password'] ?? '';

        if (!isset($users[$username])) {
            $logger->warning("Authentication failed: user not found", ['username' => $username]);
            return false;
        }

        if (password_verify($password, $users[$username]['password'])) {
            $logger->info("User authenticated successfully", ['username' => $username]);
            return true;
        }

        $logger->warning("Authentication failed: invalid password", ['username' => $username]);
        return false;
    },
    'error' => function (Response $response, array $arguments): Response {
        $data = [
            'status' => 'error',
            'message' => $arguments['message'] ?? 'Authentication failed',
            'timestamp' => date('c')
        ];

        $response->getBody()->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
    }
]));

What’s new:

  • Type hints for better code quality
  • Structured error responses with timestamps
  • Logging for security auditing
  • relaxed mode for local development
  • Proper HTTP status codes

Step 4: Creating API Routes

Health Check Endpoint

// Health check endpoint (useful for monitoring)
$app->get('/health', function (Request $request, Response $response) {
    $data = [
        'status' => 'healthy',
        'timestamp' => date('c'),
        'version' => '2.0.0'
    ];

    $response->getBody()->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
    return $response->withHeader('Content-Type', 'application/json');
});

Essential for monitoring and load balancers.

Authenticated User Greeting

// Greeting endpoint with authenticated user
$app->get('/hello', function (Request $request, Response $response) use ($logger) {
    $users = getAuthenticatedUsers();
    $headers = $request->getHeaders();
    $httpAuthUser = $headers['PHP_AUTH_USER'][0] ?? null;

    if (!$httpAuthUser || !isset($users[$httpAuthUser])) {
        $data = [
            'status' => 'error',
            'message' => 'User not found',
            'timestamp' => date('c')
        ];
        $response->getBody()->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
    }

    $logger->info("Hello endpoint accessed", ['username' => $httpAuthUser]);

    $data = [
        'status' => 'success',
        'greeting' => 'Hello, ' . $users[$httpAuthUser]['display_name'],
        'user' => [
            'username' => $httpAuthUser,
            'display_name' => $users[$httpAuthUser]['display_name'],
            'email' => $users[$httpAuthUser]['email']
        ],
        'timestamp' => date('c')
    ];

    $response->getBody()->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
    return $response->withHeader('Content-Type', 'application/json');
});

User Info Endpoint

// Get current user info
$app->get('/user/me', function (Request $request, Response $response) use ($logger) {
    $users = getAuthenticatedUsers();
    $headers = $request->getHeaders();
    $httpAuthUser = $headers['PHP_AUTH_USER'][0] ?? null;

    if (!$httpAuthUser || !isset($users[$httpAuthUser])) {
        $data = ['status' => 'error', 'message' => 'User not found'];
        $response->getBody()->write(json_encode($data));
        return $response->withHeader('Content-Type', 'application/json')->withStatus(404);
    }

    $data = [
        'status' => 'success',
        'user' => [
            'id' => $users[$httpAuthUser]['id'],
            'username' => $httpAuthUser,
            'display_name' => $users[$httpAuthUser]['display_name'],
            'email' => $users[$httpAuthUser]['email']
        ]
    ];

    $response->getBody()->write(json_encode($data, JSON_PRETTY_PRINT));
    return $response->withHeader('Content-Type', 'application/json');
});

WordPress Posts Endpoint

// Get WordPress posts with pagination
$app->get('/posts', function (Request $request, Response $response) use ($logger) {
    $queryParams = $request->getQueryParams();
    $limit = (int)($queryParams['limit'] ?? 10);
    $offset = (int)($queryParams['offset'] ?? 0);

    $posts = get_posts([
        'posts_per_page' => min($limit, 100),
        'offset' => $offset,
        'post_status' => 'publish'
    ]);

    $formattedPosts = array_map(function ($post) {
        return [
            'id' => $post->ID,
            'title' => $post->post_title,
            'content' => $post->post_content,
            'excerpt' => $post->post_excerpt,
            'slug' => $post->post_name,
            'date' => $post->post_date,
            'author' => get_the_author_meta('display_name', $post->post_author)
        ];
    }, $posts);

    $data = [
        'status' => 'success',
        'posts' => $formattedPosts,
        'meta' => [
            'count' => count($formattedPosts),
            'limit' => $limit,
            'offset' => $offset
        ]
    ];

    $response->getBody()->write(json_encode($data, JSON_PRETTY_PRINT));
    return $response->withHeader('Content-Type', 'application/json');
});

Run the Application

// Run the application
$app->run();

Step 5: Testing Your API

Using cURL

# Health check (no auth required)
curl https://api.example.com/health

# Authenticated request
curl -u username:password https://api.example.com/hello

# Get user info
curl -u username:password https://api.example.com/user/me

# Get posts with pagination
curl -u username:password "https://api.example.com/posts?limit=5&offset=0"

Expected Response

{
  "status": "success",
  "greeting": "Hello, Krasen Slavov",
  "user": {
    "username": "krasen",
    "display_name": "Krasen Slavov",
    "email": "krasen@example.com"
  },
  "timestamp": "2025-12-02T10:30:00+00:00"
}

Modern PHP 8.1+ Features We Used

  1. Strict Types – declare(strict_types=1)
  2. Union Types – Used in function signatures
  3. Null Coalescing – ?? operator for default values
  4. Arrow Functions – Shorter anonymous functions
  5. Static Variables – For caching within functions

Security Best Practices (2025)

  1. Always use HTTPS – Set AUTH_SECURE=true in production
  2. Environment Variables – Never hardcode credentials
  3. Input Validation – Sanitize all user input
  4. Rate Limiting – Consider adding for production
  5. Logging – Monitor authentication attempts
  6. CORS Configuration – Restrict to specific origins in production
  7. Regular Updates – Keep dependencies updated with composer update

What’s Next?

Now that you have a working API, consider:

  1. Adding More Endpoints – Custom post types, taxonomies, user management
  2. Implementing JWT Authentication – For stateless authentication
  3. Adding Rate Limiting – Prevent abuse
  4. Caching – Redis or Memcached for performance
  5. API Documentation – Using OpenAPI/Swagger
  6. Unit Testing – PHPUnit for robust code
  7. CI/CD Pipeline – Automated testing and deployment

Conclusion

You’ve built a modern, production-ready API server that:

  • Uses the latest Slim Framework 4
  • Follows PHP 8.1+ best practices
  • Implements proper security and logging
  • Integrates seamlessly with WordPress
  • Provides a foundation for complex API needs

The full source code is available on GitHub: https://github.com/krasenslavov/api-server-slim-wordpress

Resources

Questions?

If you have any questions or run into issues, feel free to:

Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *