
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
relaxedmode 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
- Strict Types –
declare(strict_types=1) - Union Types – Used in function signatures
- Null Coalescing –
??operator for default values - Arrow Functions – Shorter anonymous functions
- Static Variables – For caching within functions
Security Best Practices (2025)
- Always use HTTPS – Set
AUTH_SECURE=truein production - Environment Variables – Never hardcode credentials
- Input Validation – Sanitize all user input
- Rate Limiting – Consider adding for production
- Logging – Monitor authentication attempts
- CORS Configuration – Restrict to specific origins in production
- Regular Updates – Keep dependencies updated with
composer update
What’s Next?
Now that you have a working API, consider:
- Adding More Endpoints – Custom post types, taxonomies, user management
- Implementing JWT Authentication – For stateless authentication
- Adding Rate Limiting – Prevent abuse
- Caching – Redis or Memcached for performance
- API Documentation – Using OpenAPI/Swagger
- Unit Testing – PHPUnit for robust code
- 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
- Slim Framework Documentation
- PSR-7 HTTP Message Interface
- Tuupola HTTP Basic Authentication
- Monolog Documentation
- WordPress Developer Resources
Questions?
If you have any questions or run into issues, feel free to:
- Open an issue on GitHub
- Connect with me on GitHub @krasenslavov
Happy coding!

