PHP JSON API — Building RESTful Endpoints with JSON Requests and Responses
In this tutorial, you will learn about PHP JSON API. We cover key concepts, practical examples, and best practices to help you master this topic.
PHP JSON APIs receive structured data through HTTP requests, process it server-side, and return JSON responses with appropriate status codes, forming the backbone of modern web service architecture.
Why It Matters
Modern web applications communicate through JSON APIs. A single-page JavaScript app, a mobile app, and a third-party integration all talk to the same backend API. PHP's built-in JSON functions make it straightforward to build these APIs. Understanding request parsing, response formatting, error handling, and CORS is essential for building APIs that external developers can consume reliably.
Real-World Use
Laravel builds RESTful APIs with resource controllers. Symfony's API Platform generates OpenAPI documentation automatically. WordPress's REST API lets plugins interact with sites programmatically. Shopify and Stripe provide PHP libraries that consume JSON APIs. DodaTech's internal services communicate through JSON APIs for user management, license verification, and scan reporting.
What You Will Learn
- JSON encoding with json_encode and decoding with json_decode
- Building RESTful endpoints that accept JSON input
- Request method handling (GET, POST, PUT, DELETE)
- Response formatting with proper status codes
- Error handling and validation in APIs
- CORS headers for cross-origin requests
- API authentication basics
Learning Path
flowchart LR A[File Upload] --> B[JSON API
You are here] B --> C[Error Handling] C --> D[Classes and OOP] D --> E[Inheritance and Traits] style B fill:#f90,color:#fff
JSON Encoding and Decoding
PHP provides built-in functions for JSON conversion:
<?php
// Encode PHP array to JSON string
$data = [
'name' => 'Alice',
'age' => 30,
'skills' => ['PHP', 'Python', 'JavaScript'],
'active' => true,
'address' => null,
];
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
echo $json;
Expected output:
{
"name": "Alice",
"age": 30,
"skills": ["PHP", "Python", "JavaScript"],
"active": true,
"address": null
}
Decode JSON to PHP
<?php
$jsonString = '{"name":"Alice","age":30,"active":true}';
// Decode to associative array
$array = json_decode($jsonString, true);
echo $array['name']; // Alice
// Decode to object
$object = json_decode($jsonString);
echo $object->name; // Alice
Error Handling for JSON Operations
<?php
function safeJsonEncode(mixed $data, int $options = 0): string {
$result = json_encode($data, $options);
if ($result === false) {
throw new \RuntimeException(
'JSON encode error: ' . json_last_error_msg()
);
}
return $result;
}
function safeJsonDecode(string $json, bool $assoc = true): mixed {
$result = json_decode($json, $assoc);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException(
'JSON decode error: ' . json_last_error_msg()
);
}
return $result;
}
Basic JSON API Router
A simple API that handles different HTTP methods and routes:
<?php
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
// Handle CORS preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$pathParts = explode('/', trim($path, '/'));
// Remove 'api' prefix if present
if ($pathParts[0] === 'api') {
array_shift($pathParts);
}
$resource = $pathParts[0] ?? '';
$id = $pathParts[1] ?? null;
// Simple in-memory data store
$items = [
1 => ['id' => 1, 'title' => 'Learn PHP', 'done' => false],
2 => ['id' => 2, 'title' => 'Build API', 'done' => false],
];
// Router
try {
switch ($resource) {
case 'items':
handleItems($method, $id, $items);
break;
case 'health':
jsonResponse(['status' => 'ok', 'time' => date('c')]);
break;
default:
jsonError('Not found', 404);
}
} catch (\Throwable $e) {
jsonError($e->getMessage(), 500);
}
RESTful Endpoint Patterns
GET All Resources
<?php
function getItems(array $items): void {
$search = $_GET['search'] ?? '';
if ($search) {
$items = array_filter($items, function($item) use ($search) {
return stripos($item['title'], $search) !== false;
});
}
jsonResponse(['data' => array_values($items)]);
}
GET Single Resource
<?php
function getItem(int $id, array $items): void {
if (!isset($items[$id])) {
jsonError('Item not found', 404);
}
jsonResponse(['data' => $items[$id]]);
}
POST Create Resource
<?php
function createItem(array &$items): void {
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['title'])) {
jsonError('Title is required', 400);
}
$id = count($items) + 1;
$items[$id] = [
'id' => $id,
'title' => $input['title'],
'done' => $input['done'] ?? false,
];
jsonResponse(['data' => $items[$id], 'message' => 'Created'], 201);
}
PUT Update Resource
<?php
function updateItem(int $id, array &$items): void {
if (!isset($items[$id])) {
jsonError('Item not found', 404);
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
jsonError('Invalid JSON body', 400);
}
if (isset($input['title'])) {
$items[$id]['title'] = $input['title'];
}
if (isset($input['done'])) {
$items[$id]['done'] = (bool)$input['done'];
}
jsonResponse(['data' => $items[$id], 'message' => 'Updated']);
}
DELETE Resource
<?php
function deleteItem(int $id, array &$items): void {
if (!isset($items[$id])) {
jsonError('Item not found', 404);
}
unset($items[$id]);
jsonResponse(['message' => 'Deleted'], 200);
}
Router Handler
<?php
function handleItems(string $method, ?string $id, array &$items): void {
switch ($method) {
case 'GET':
$id ? getItem((int)$id, $items) : getItems($items);
break;
case 'POST':
createItem($items);
break;
case 'PUT':
$id ? updateItem((int)$id, $items) : jsonError('ID required', 400);
break;
case 'DELETE':
$id ? deleteItem((int)$id, $items) : jsonError('ID required', 400);
break;
default:
jsonError('Method not allowed', 405);
}
}
function jsonResponse(mixed $data, int $status = 200): never {
http_response_code($status);
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
function jsonError(string $message, int $status = 400): never {
jsonResponse(['error' => true, 'message' => $message], $status);
}
Reading JSON Request Body
AJAX and mobile apps send JSON in the request body, not as form data:
<?php
// Read raw JSON input
$rawInput = file_get_contents('php://input');
$data = json_decode($rawInput, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON: ' . json_last_error_msg()]);
exit;
}
// Validate required fields
$required = ['email', 'password'];
$missing = [];
foreach ($required as $field) {
if (empty($data[$field])) {
$missing[] = $field;
}
}
if ($missing) {
http_response_code(422);
echo json_encode([
'error' => 'Validation failed',
'missing_fields' => $missing,
]);
exit;
}
// Process the request
echo json_encode(['status' => 'success', 'message' => 'Login successful']);
Pagination in APIs
<?php
function paginatedResponse(array $allItems, int $page, int $perPage): array {
$total = count($allItems);
$totalPages = ceil($total / $perPage);
$offset = ($page - 1) * $perPage;
$items = array_slice($allItems, $offset, $perPage);
return [
'data' => $items,
'pagination' => [
'current_page' => $page,
'per_page' => $perPage,
'total' => $total,
'total_pages' => $totalPages,
'has_next' => $page < $totalPages,
'has_prev' => $page > 1,
],
];
}
// Usage: GET /api/items?page=2&per_page=10
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = min(100, max(1, (int)($_GET['per_page'] ?? 10)));
API Authentication with Bearer Token
<?php
function authenticate(): array {
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!preg_match('/^Bearer\s+(.+)$/i', $header, $matches)) {
jsonError('Missing or invalid authorization header', 401);
}
$token = $matches[1];
// Validate token against database
$stmt = $pdo->prepare('SELECT * FROM api_tokens WHERE token = ? AND expires > NOW()');
$stmt->execute([hash('sha256', $token)]);
$user = $stmt->fetch();
if (!$user) {
jsonError('Invalid or expired token', 401);
}
return $user;
}
// Usage in protected endpoint
$user = authenticate();
jsonResponse(['message' => 'Welcome, ' . $user['name']]);
Common Mistakes
1. Not Setting Content-Type Header
echo json_encode($data); // Browser may interpret as text/html
Always set header('Content-Type: application/json') before JSON output.
2. Returning Success Status for Errors
http_response_code(200); // Wrong for errors
echo json_encode(['error' => 'Validation failed']);
Use appropriate status codes: 400 for bad request, 401 for unauthorized, 404 for not found, 422 for validation errors.
3. Not Validating JSON Input
$data = json_decode(file_get_contents('php://input'), true);
// $data could be null if JSON is invalid
Always check json_last_error() after decoding.
4. Exposing Internal Errors to Clients
jsonResponse(['error' => $e->getMessage()]); // May leak sensitive info
Log the full error server-side, return a generic message to the client.
5. Ignoring CORS for Cross-Origin Requests
If your API is called from a different domain, the browser blocks the request without Access-Control-Allow-Origin headers.
Practice Questions
What does file_get_contents('php://input') do? It reads the raw HTTP request body, which contains JSON data for API requests.
Why should you use JSON_UNESCAPED_UNICODE in json_encode? It prevents escaping Unicode characters like emoji and non-Latin scripts as \uXXXX sequences.
What HTTP status code should a successful POST return? 201 Created. A successful GET returns 200. A validation error returns 422.
How do you read query string parameters in an API? Use $_GET['param'] ?? 'default'. For complex filtering, parse the query string from $_SERVER['QUERY_STRING'].
Challenge: Build a complete REST API for a task manager with CRUD operations, pagination, search, and Bearer token authentication.
Mini Project: Todo API
Build a simple todo list API:
<?php
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$dataFile = __DIR__ . '/todos.json';
function loadTodos(): array {
global $dataFile;
return file_exists($dataFile) ? json_decode(file_get_contents($dataFile), true) : [];
}
function saveTodos(array $todos): void {
global $dataFile;
file_put_contents($dataFile, json_encode($todos, JSON_PRETTY_PRINT));
}
$method = $_SERVER['REQUEST_METHOD'];
$input = json_decode(file_get_contents('php://input'), true);
switch ($method) {
case 'GET':
$todos = loadTodos();
echo json_encode(['data' => $todos]);
break;
case 'POST':
if (empty($input['title'])) {
http_response_code(422);
echo json_encode(['error' => 'Title is required']);
exit;
}
$todos = loadTodos();
$todo = ['id' => uniqid(), 'title' => $input['title'], 'done' => false];
$todos[] = $todo;
saveTodos($todos);
http_response_code(201);
echo json_encode(['data' => $todo]);
break;
case 'PUT':
$todos = loadTodos();
foreach ($todos as &$todo) {
if ($todo['id'] === ($input['id'] ?? '')) {
$todo['title'] = $input['title'] ?? $todo['title'];
$todo['done'] = $input['done'] ?? $todo['done'];
echo json_encode(['data' => $todo]);
saveTodos($todos);
exit;
}
}
http_response_code(404);
echo json_encode(['error' => 'Not found']);
break;
case 'DELETE':
$todos = loadTodos();
$todos = array_values(array_filter($todos, fn($t) => $t['id'] !== ($input['id'] ?? '')));
saveTodos($todos);
echo json_encode(['message' => 'Deleted']);
break;
default:
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
FAQ
What is Next
Now that you can build APIs, proceed to Error Handling to learn robust error management with try/catch and custom error handlers. Then read Classes and OOP to organize your code with object-oriented programming.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro