12: REST APIs - Express.js vs PHP Native
REST APIs: Express.js vs PHP Native
Section titled “REST APIs: Express.js vs PHP Native”Overview
Section titled “Overview”If you’ve built APIs with Express.js, you’ll find PHP’s approach familiar. This chapter shows you how to build REST APIs in native PHP, then compares it to using Slim Framework (PHP’s Express equivalent). We’ll cover routing, middleware, validation, and best practices.
Learning Objectives
Section titled “Learning Objectives”By the end of this chapter, you’ll be able to:
- ✅ Build REST APIs without frameworks
- ✅ Implement routing (like Express routes)
- ✅ Create middleware pipelines
- ✅ Handle JSON requests/responses
- ✅ Validate input data
- ✅ Use Slim Framework (PHP’s Express)
- ✅ Apply RESTful best practices
- ✅ Handle errors properly
Code Examples
Section titled “Code Examples”📁 View Code Examples on GitHub
This chapter includes complete REST API examples:
01-native-api.php- Basic REST API without frameworks02-router-class.php- Custom router implementation03-request-response.php- Clean request/response objects04-middleware.php- Middleware pipeline pattern05-slim-api/- Full CRUD API with Slim Framework06-validation.php- Input validation examples
Run the examples:
cd code/php-typescript-developers/chapter-12php -S localhost:8000 01-native-api.phpcurl http://localhost:8000/api/usersQuick Comparison
Section titled “Quick Comparison”| Feature | Express.js | PHP Native | Slim Framework |
|---|---|---|---|
| Routing | app.get('/users') | Manual routing | $app->get('/users') |
| Middleware | app.use() | Manual | ->add() |
| JSON Parsing | express.json() | json_decode() | Automatic |
| Response | res.json() | echo json_encode() | $response->withJson() |
| Parameters | req.params.id | $_GET, $_POST | $request->getAttribute() |
Basic REST API
Section titled “Basic REST API”Express.js API
Section titled “Express.js API”const express = require('express');const app = express();
app.use(express.json());
// GET /api/usersapp.get('/api/users', (req, res) => { const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' } ]; res.json(users);});
// GET /api/users/:idapp.get('/api/users/:id', (req, res) => { const id = parseInt(req.params.id); const user = { id, name: 'Alice' }; res.json(user);});
// POST /api/usersapp.post('/api/users', (req, res) => { const { name, email } = req.body; const user = { id: 3, name, email }; res.status(201).json(user);});
app.listen(3000, () => { console.log('Server running on port 3000');});PHP Native API
Section titled “PHP Native API”<?phpheader('Content-Type: application/json');
$method = $_SERVER['REQUEST_METHOD'];$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Simple routerif ($method === 'GET' && $path === '/api/users') { $users = [ ['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob'] ]; echo json_encode($users); exit;}
if ($method === 'GET' && preg_match('#^/api/users/(\d+)$#', $path, $matches)) { $id = (int) $matches[1]; $user = ['id' => $id, 'name' => 'Alice']; echo json_encode($user); exit;}
if ($method === 'POST' && $path === '/api/users') { $data = json_decode(file_get_contents('php://input'), true); $user = [ 'id' => 3, 'name' => $data['name'] ?? '', 'email' => $data['email'] ?? '' ]; http_response_code(201); echo json_encode($user); exit;}
// 404 Not Foundhttp_response_code(404);echo json_encode(['error' => 'Not Found']);Run:
php -S localhost:8000Building a Router
Section titled “Building a Router”Express.js Router
Section titled “Express.js Router”const express = require('express');const router = express.Router();
router.get('/', (req, res) => { res.json({ message: 'User list' });});
router.post('/', (req, res) => { res.json({ message: 'User created' });});
app.use('/api/users', router);PHP Router Class
Section titled “PHP Router Class”<?phpdeclare(strict_types=1);
class Router { private array $routes = [];
public function get(string $path, callable $handler): void { $this->addRoute('GET', $path, $handler); }
public function post(string $path, callable $handler): void { $this->addRoute('POST', $path, $handler); }
public function put(string $path, callable $handler): void { $this->addRoute('PUT', $path, $handler); }
public function delete(string $path, callable $handler): void { $this->addRoute('DELETE', $path, $handler); }
private function addRoute(string $method, string $path, callable $handler): void { $this->routes[] = [ 'method' => $method, 'path' => $path, 'handler' => $handler ]; }
public function dispatch(): void { $method = $_SERVER['REQUEST_METHOD']; $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($this->routes as $route) { if ($route['method'] !== $method) { continue; }
$pattern = $this->convertPathToRegex($route['path']); if (preg_match($pattern, $path, $matches)) { array_shift($matches); // Remove full match call_user_func_array($route['handler'], $matches); return; } }
http_response_code(404); echo json_encode(['error' => 'Not Found']); }
private function convertPathToRegex(string $path): string { // Convert /users/:id to /users/(\d+) $pattern = preg_replace('#:([a-zA-Z]+)#', '([^/]+)', $path); return '#^' . $pattern . '$#'; }}Usage:
<?phprequire 'Router.php';
header('Content-Type: application/json');
$router = new Router();
$router->get('/api/users', function() { echo json_encode([ ['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob'] ]);});
$router->get('/api/users/:id', function($id) { echo json_encode(['id' => $id, 'name' => 'Alice']);});
$router->post('/api/users', function() { $data = json_decode(file_get_contents('php://input'), true); http_response_code(201); echo json_encode(['id' => 3, 'name' => $data['name']]);});
$router->dispatch();Middleware
Section titled “Middleware”Express.js Middleware
Section titled “Express.js Middleware”// Logging middlewareapp.use((req, res, next) => { console.log(`${req.method} ${req.path}`); next();});
// Auth middlewareconst requireAuth = (req, res, next) => { const token = req.headers.authorization; if (!token) { return res.status(401).json({ error: 'Unauthorized' }); } next();};
app.get('/api/protected', requireAuth, (req, res) => { res.json({ message: 'Protected data' });});PHP Middleware
Section titled “PHP Middleware”<?phpclass Router { private array $middleware = [];
public function use(callable $middleware): void { $this->middleware[] = $middleware; }
public function dispatch(): void { // Run middleware chain $this->runMiddleware(0, function() { // Then dispatch route $this->matchRoute(); }); }
private function runMiddleware(int $index, callable $next): void { if ($index >= count($this->middleware)) { $next(); return; }
$middleware = $this->middleware[$index]; $middleware(function() use ($index, $next) { $this->runMiddleware($index + 1, $next); }); }}
// Usage$router = new Router();
// Logging middleware$router->use(function($next) { $method = $_SERVER['REQUEST_METHOD']; $path = $_SERVER['REQUEST_URI']; error_log("{$method} {$path}"); $next();});
// Auth middleware$requireAuth = function($next) { $token = $_SERVER['HTTP_AUTHORIZATION'] ?? null; if (!$token) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit; } $next();};
$router->use($requireAuth);
$router->get('/api/protected', function() { echo json_encode(['message' => 'Protected data']);});Request/Response Objects
Section titled “Request/Response Objects”Express.js
Section titled “Express.js”app.post('/api/users', (req, res) => { // Request const body = req.body; // Parsed JSON const query = req.query; // Query params const params = req.params; // Route params const headers = req.headers; // Headers
// Response res.status(201) .header('X-Custom', 'value') .json({ success: true });});PHP Request/Response Classes
Section titled “PHP Request/Response Classes”<?phpclass Request { public function body(): array { return json_decode(file_get_contents('php://input'), true) ?? []; }
public function query(string $key = null): mixed { return $key ? ($_GET[$key] ?? null) : $_GET; }
public function header(string $name): ?string { $name = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); return $_SERVER[$name] ?? null; }
public function method(): string { return $_SERVER['REQUEST_METHOD']; }
public function path(): string { return parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); }}
class Response { private int $statusCode = 200; private array $headers = [];
public function status(int $code): self { $this->statusCode = $code; return $this; }
public function header(string $name, string $value): self { $this->headers[$name] = $value; return $this; }
public function json(array $data): void { http_response_code($this->statusCode); foreach ($this->headers as $name => $value) { header("{$name}: {$value}"); } header('Content-Type: application/json'); echo json_encode($data); }}
// Usage$req = new Request();$res = new Response();
$res->status(201) ->header('X-Custom', 'value') ->json(['success' => true]);Slim Framework (PHP’s Express)
Section titled “Slim Framework (PHP’s Express)”Slim is a microframework inspired by Sinatra/Express:
Installation
Section titled “Installation”composer require slim/slim slim/psr7Slim API
Section titled “Slim API”<?phprequire 'vendor/autoload.php';
use Slim\Factory\AppFactory;use Psr\Http\Message\ResponseInterface as Response;use Psr\Http\Message\ServerRequestInterface as Request;
$app = AppFactory::create();
// GET /api/users$app->get('/api/users', function (Request $request, Response $response) { $users = [ ['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob'] ];
$response->getBody()->write(json_encode($users)); return $response->withHeader('Content-Type', 'application/json');});
// GET /api/users/{id}$app->get('/api/users/{id}', function (Request $request, Response $response, $args) { $id = (int) $args['id']; $user = ['id' => $id, 'name' => 'Alice'];
$response->getBody()->write(json_encode($user)); return $response->withHeader('Content-Type', 'application/json');});
// POST /api/users$app->post('/api/users', function (Request $request, Response $response) { $data = json_decode($request->getBody()->getContents(), true);
$user = [ 'id' => 3, 'name' => $data['name'] ?? '', 'email' => $data['email'] ?? '' ];
$response->getBody()->write(json_encode($user)); return $response ->withHeader('Content-Type', 'application/json') ->withStatus(201);});
$app->run();Run:
php -S localhost:8000 -t public/Validation
Section titled “Validation”Express.js with Joi
Section titled “Express.js with Joi”const Joi = require('joi');
const userSchema = Joi.object({ name: Joi.string().required(), email: Joi.string().email().required(), age: Joi.number().min(18)});
app.post('/api/users', (req, res) => { const { error } = userSchema.validate(req.body); if (error) { return res.status(400).json({ errors: error.details }); }
// Create user... res.json({ success: true });});PHP with respect/validation
Section titled “PHP with respect/validation”composer require respect/validation<?phpuse Respect\Validation\Validator as v;
$app->post('/api/users', function (Request $request, Response $response) { $data = json_decode($request->getBody()->getContents(), true);
$validator = v::key('name', v::stringType()->notEmpty()) ->key('email', v::email()) ->key('age', v::intType()->min(18));
try { $validator->assert($data); } catch (\Respect\Validation\Exceptions\ValidationException $e) { $response->getBody()->write(json_encode([ 'errors' => $e->getMessages() ])); return $response ->withHeader('Content-Type', 'application/json') ->withStatus(400); }
// Create user... $response->getBody()->write(json_encode(['success' => true])); return $response->withHeader('Content-Type', 'application/json');});Error Handling
Section titled “Error Handling”Express.js Error Middleware
Section titled “Express.js Error Middleware”// Error handler (must be last)app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Internal Server Error', message: err.message });});Slim Error Handling
Section titled “Slim Error Handling”<?phpuse Slim\Exception\HttpNotFoundException;use Slim\Handlers\ErrorHandler;
$customErrorHandler = function ( ServerRequestInterface $request, Throwable $exception, bool $displayErrorDetails) use ($app) { $response = $app->getResponseFactory()->createResponse();
$payload = [ 'error' => get_class($exception), 'message' => $exception->getMessage() ];
$response->getBody()->write(json_encode($payload));
return $response ->withHeader('Content-Type', 'application/json') ->withStatus(500);};
$errorMiddleware = $app->addErrorMiddleware(true, true, true);$errorMiddleware->setDefaultErrorHandler($customErrorHandler);RESTful Best Practices
Section titled “RESTful Best Practices”Resource Naming
Section titled “Resource Naming”// ✅ GoodGET /api/users // List usersGET /api/users/1 // Get userPOST /api/users // Create userPUT /api/users/1 // Update userDELETE /api/users/1 // Delete user
// ❌ BadGET /api/getUsersPOST /api/createUserGET /api/user_detail/1HTTP Status Codes
Section titled “HTTP Status Codes”| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, DELETE |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE (no body) |
| 400 | Bad Request | Validation error |
| 401 | Unauthorized | Missing/invalid auth |
| 403 | Forbidden | Auth valid but insufficient permissions |
| 404 | Not Found | Resource doesn’t exist |
| 422 | Unprocessable Entity | Validation error (alternative to 400) |
| 500 | Internal Server Error | Server error |
Response Format
Section titled “Response Format”Successful Response:
{ "data": { "id": 1, "name": "Alice", "email": "alice@example.com" }}Error Response:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid input data", "details": [ { "field": "email", "message": "Invalid email format" } ] }}CORS Handling
Section titled “CORS Handling”Express.js:
const cors = require('cors');app.use(cors());PHP:
<?phpheader('Access-Control-Allow-Origin: *');header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit;}Complete CRUD Example
Section titled “Complete CRUD Example”Express.js
Section titled “Express.js”const express = require('express');const app = express();app.use(express.json());
let users = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }];let nextId = 3;
// Listapp.get('/api/users', (req, res) => { res.json({ data: users });});
// Getapp.get('/api/users/:id', (req, res) => { const user = users.find(u => u.id === parseInt(req.params.id)); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json({ data: user });});
// Createapp.post('/api/users', (req, res) => { const user = { id: nextId++, ...req.body }; users.push(user); res.status(201).json({ data: user });});
// Updateapp.put('/api/users/:id', (req, res) => { const index = users.findIndex(u => u.id === parseInt(req.params.id)); if (index === -1) { return res.status(404).json({ error: 'User not found' }); } users[index] = { ...users[index], ...req.body }; res.json({ data: users[index] });});
// Deleteapp.delete('/api/users/:id', (req, res) => { const index = users.findIndex(u => u.id === parseInt(req.params.id)); if (index === -1) { return res.status(404).json({ error: 'User not found' }); } users.splice(index, 1); res.status(204).send();});
app.listen(3000);Slim Framework
Section titled “Slim Framework”<?phprequire 'vendor/autoload.php';
use Slim\Factory\AppFactory;use Psr\Http\Message\ResponseInterface as Response;use Psr\Http\Message\ServerRequestInterface as Request;
$app = AppFactory::create();$app->addBodyParsingMiddleware();$app->addRoutingMiddleware();$app->addErrorMiddleware(true, true, true);
$users = [ ['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com'], ['id' => 2, 'name' => 'Bob', 'email' => 'bob@example.com']];$nextId = 3;
// List$app->get('/api/users', function (Request $request, Response $response) use (&$users) { $response->getBody()->write(json_encode(['data' => $users])); return $response->withHeader('Content-Type', 'application/json');});
// Get$app->get('/api/users/{id}', function (Request $request, Response $response, $args) use (&$users) { $id = (int) $args['id']; $user = array_values(array_filter($users, fn($u) => $u['id'] === $id))[0] ?? null;
if (!$user) { $response->getBody()->write(json_encode(['error' => 'User not found'])); return $response->withHeader('Content-Type', 'application/json')->withStatus(404); }
$response->getBody()->write(json_encode(['data' => $user])); return $response->withHeader('Content-Type', 'application/json');});
// Create$app->post('/api/users', function (Request $request, Response $response) use (&$users, &$nextId) { $data = $request->getParsedBody(); $user = ['id' => $nextId++] + $data; $users[] = $user;
$response->getBody()->write(json_encode(['data' => $user])); return $response->withHeader('Content-Type', 'application/json')->withStatus(201);});
// Update$app->put('/api/users/{id}', function (Request $request, Response $response, $args) use (&$users) { $id = (int) $args['id']; $data = $request->getParsedBody();
$index = array_search($id, array_column($users, 'id')); if ($index === false) { $response->getBody()->write(json_encode(['error' => 'User not found'])); return $response->withHeader('Content-Type', 'application/json')->withStatus(404); }
$users[$index] = array_merge($users[$index], $data);
$response->getBody()->write(json_encode(['data' => $users[$index]])); return $response->withHeader('Content-Type', 'application/json');});
// Delete$app->delete('/api/users/{id}', function (Request $request, Response $response, $args) use (&$users) { $id = (int) $args['id']; $index = array_search($id, array_column($users, 'id'));
if ($index === false) { $response->getBody()->write(json_encode(['error' => 'User not found'])); return $response->withHeader('Content-Type', 'application/json')->withStatus(404); }
array_splice($users, $index, 1);
return $response->withStatus(204);});
$app->run();Key Takeaways
Section titled “Key Takeaways”- PHP can build REST APIs just like Express.js with similar patterns
- Slim Framework is PHP’s Express equivalent - lightweight and minimal
- Routing patterns are nearly identical (verb + path + handler)
- Middleware works the same way (request → middleware chain → response)
- PSR-7 provides standard request/response objects across frameworks
- Native PHP is verbose but gives full control without framework overhead
- For production, use Laravel or Symfony for full-featured APIs
- Laravel API resources transform models to JSON (like Express serializers)
- Route model binding automatically injects models by ID (Laravel feature)
- API versioning typically done via URL prefix (
/api/v1/) or headers - CORS middleware needed for frontend consumption - built into most frameworks
- Use
apiResourcein Laravel for instant RESTful routes (index, store, show, update, destroy)
Comparison Summary
Section titled “Comparison Summary”| Feature | Express.js | Slim Framework |
|---|---|---|
| Setup | npm install express | composer require slim/slim |
| Routing | app.get('/users') | $app->get('/users') |
| Middleware | app.use(middleware) | $app->add(middleware) |
| Request | req.body | $request->getParsedBody() |
| Response | res.json() | $response->withJson() |
| Error Handling | Error middleware | Error middleware |
| Ecosystem | Large | Moderate |
Next Steps
Section titled “Next Steps”Now that you understand REST APIs, let’s dive into Laravel—PHP’s most popular framework.
Next Chapter: 13: Laravel Foundations: The PHP Framework
Resources
Section titled “Resources”- Slim Framework
- PSR-7: HTTP Message Interface
- FastRoute - Router library
- Respect/Validation
- REST API Design Best Practices
Questions or feedback? Open an issue on GitHub