This article addresses a specific, maddening PHP bug: your REST API returns a 200 OK with an empty body, and the culprit is a missing Content-Type: application/json header. You'll learn why PHP's $_POST stays empty when JSON arrives without the correct header, and how to catch that with php://input instead.
★
Imagine a restaurant.
The piece walks through building a minimal, framework-free REST API from scratch—routing requests manually, parsing JSON bodies, validating inputs, and returning proper HTTP status codes. It covers a lightweight API key authentication scheme using headers and database integration via PDO with prepared statements.
This is for developers who want to understand the raw mechanics of HTTP request handling in PHP without the abstraction of Laravel or Symfony. Don't use this approach for complex APIs with dozens of endpoints—you'll want a router library or micro-framework like Slim or Flight.
But for a single-purpose API, a learning exercise, or a situation where you can't install Composer dependencies, this gives you full control and zero overhead.
Plain-English First
Imagine a restaurant. You're the customer, your app is the waiter, and the kitchen is the server. A REST API is just the menu and the rules for how you ask the kitchen for things — 'GET me a burger', 'POST a new order', 'DELETE that side of fries'. PHP is the kitchen staff that reads your order, prepares it, and sends back a plate (JSON data). No fancy equipment needed — just the basics.
Every app you use daily — Instagram, Spotify, your bank — is powered by APIs running quietly in the background. When your phone loads your feed, it's making a REST API call. Understanding how to build one from scratch, without hiding behind Laravel or Symfony, is what separates developers who use tools from developers who understand them.
Frameworks are great, but they can mask what's really happening. When something breaks in production at 2am, you need to know what's underneath. Pure PHP API development forces you to confront the raw HTTP request lifecycle — how a URL becomes a route, how a method becomes an action, and how your data becomes a JSON response. That understanding makes you a better developer regardless of which framework you use later.
By the end of this article you'll have a fully working REST API in PHP — with routing, all four HTTP methods (GET, POST, PUT, DELETE), proper status codes, input validation, and API key authentication — built with nothing but PHP itself. No Composer packages. No magic. Just clean, readable code you fully understand.
When a mobile app returns an empty response and you have no framework to trace the issue, knowing how raw PHP handles requests becomes your only lifeline.
That's the real difference: you stop guessing and start fixing. Build it once from scratch, and every framework you touch after will feel like a tool you control, not a black box you trust.
PHP REST API Without Content-Type: The Silent 200
A REST API in pure PHP is an HTTP endpoint that accepts a request, parses its intent (method, URI, headers), and returns a structured response — typically JSON — without any framework abstraction. The core mechanic is manual routing: you read $_SERVER['REQUEST_METHOD'] and $_SERVER['REQUEST_URI'], then dispatch to a handler that calls json_encode() and echos the result. No middleware, no autoloaded controllers — just raw PHP and header() calls.
In practice, the critical property is that the client and server must agree on the content type. If your API returns JSON but omits header('Content-Type: application/json'), the browser or HTTP client treats the response as text/html — and if the JSON is valid, it still renders as an empty page or triggers a silent parse failure. The same applies to incoming data: without checking $_SERVER['CONTENT_TYPE'], you might attempt json_decode(file_get_contents('php://input')) on a form-encoded body and get null with no error.
Use pure PHP for micro-endpoints, health checks, or when you control both client and server and need zero overhead. It matters in real systems because a missing Content-Type header is the #1 cause of “works in Postman, fails in production” — the API returns HTTP 200 with an empty body, and the client silently drops the response.
Content-Type Is Not Optional
A missing Content-Type header does not cause a PHP error — it causes the client to misinterpret the response, leading to silent data loss or empty UI states.
Production Insight
A payment webhook receiver returned HTTP 200 with an empty body because the PHP script omitted Content-Type: application/json — the upstream service interpreted the response as success but discarded the payload.
Symptom: the client logs show a 200 response with Content-Type: text/html and zero bytes of body, but the PHP script actually called json_encode() and echo.
Rule: always set Content-Type before any output, and validate it on incoming requests — reject non-JSON bodies with 415 Unsupported Media Type.
Key Takeaway
Always set Content-Type header before echoing the response body — missing it causes silent client-side failures.
Validate incoming Content-Type and reject mismatched formats with a 415 status code.
Use json_last_error() after decoding request bodies — null from json_decode is not the same as valid JSON.
thecodeforge.io
PHP REST API Without Content-Type: Silent 200
Rest Api Pure Php
Routing Requests Without a Framework
Every framework hides route matching behind a facade. In pure PHP, you parse the URL and method yourself using $_SERVER['REQUEST_METHOD'] and $_SERVER['REQUEST_URI']. The simplest router is a series of if/else if checks, but for scaling you'll want a dispatch table — an associative array mapping route patterns to handler functions.
One common production issue: trailing slashes. A route defined as /users won't match /users/ unless you normalize the URI. Always trim trailing slashes or add a redirect rule to avoid 404 surprises.
Another subtlety: query strings. REQUEST_URI includes them, so you must strip them before matching. parse_url() with PHP_URL_PATH is your friend. We've seen a production outage where a client appended ?debug=true to every request and every endpoint returned 404.
Use # as regex delimiter to avoid escaping forward slashes. It makes patterns like #^/users/(\d+)$# readable and avoids the 'leaning toothpick syndrome'.
Production Insight
A regex-based router can hit O(n) per request for n routes.
For high-traffic endpoints, compile patterns into a static array once and avoid preg_match overhead.
Rule: if you have more than 50 routes, switch to a trie-based matcher or use FastRoute.
Key Takeaway
Your router is just code — no magic, no middleware.
For < 50 routes, a regex dispatch table is clean and fast.
Performance matters: preg_match is ~1-2µs per route — 500 routes add 1ms.
Router Implementation Decision
IfFewer than 10 routes, simple patterns
→
UseUse simple if/else blocks — no regex overhead, easy to debug.
UseUse a nested trie or compile regex once at startup (e.g., FastRoute).
Handling HTTP Methods and JSON Data
PHP's $_POST only works for form-encoded data. For JSON, you must read raw input. GET params come from $_GET, but for PUT/DELETE you also read php://input. Always check Content-Type and return appropriate status codes: 200 for success, 201 for created, 204 for deleted, 400 for bad request.
Another edge case: PHP's php://input cannot be read twice. If you have middleware that reads the body for logging, you must capture it in a variable and pass it down. Otherwise, your route handler gets an empty string.
Also: remember that DELETE requests sometimes include a body for soft-delete metadata. Many developers forget to parse it and lose audit information.
file_get_contents('php://input') can only be read once. If you read it and then try to read again, you'll get an empty string. Parse it once and store in a variable.
Production Insight
Mobile apps often omit Content-Type header due to framework bugs.
If you don't validate Content-Type, an empty body passes as valid JSON 'null' and decodes to null.
Rule: always reject or coerce missing Content-Type explicitly — return 415 or default to JSON parsing.
Key Takeaway
JSON doesn't fill $_POST — read php://input.
Validate Content-Type early to avoid silent null-body bugs.
Always set Content-Type: application/json on responses.
Input Validation and Error Handling
Validation in pure PHP is manual: check each expected field, return specific error messages. Use filter_var for sanitization and custom checks for required fields. Wrap your handler in try-catch to catch PHP errors and return a structured error response. Never expose internal errors to the client in production.
Beyond field validation, always validate the structure — e.g., ensure the request body is an object, not an array at the top level. A client sending [] instead of {} can cause unexpected errors downstream. Return 400 with a clear message.
Also consider: what if the JSON is valid but contains unexpected fields? Silent ignoring can hide typos. Consider strict mode where extra fields cause a 400 with a list of unexpected keys.
The Required field check is like a 'must be 21 or older' sign.
Email validation is the ID scanner — only valid formats pass.
Error messages are the 'you can't enter' note with the reason.
Returning 400 with field-specific errors lets the client fix exactly one thing at a time.
Production Insight
If you return generic 'Invalid input' without field details, mobile apps waste hours debugging.
We once saw a 3-hour outage because a validation message said 'Bad request' — no one knew which field.
Rule: always return an array of per-field error messages in JSON.
Key Takeaway
Validate every input field explicitly — no shortcuts.
Return specific error messages per field for fast debugging.
Wrap main logic in try-catch, log the exception, and return 500 safely.
Validation Approach Decision
IfSimple forms with few fields
→
UseManual if/else checks — fast to write and easy to understand.
IfComplex nested JSON payloads
→
UseUse a validation class like above, with chaining and reusable rules.
IfMany endpoints with shared validation logic
→
UseExtract validation into reusable rules or consider a lightweight validation library.
API Key Authentication – A Minimal Approach
For production without a framework, API key authentication is straightforward: expect a header like Authorization: Bearer <key> or X-API-Key. Compare against a stored value (env variable, database, or file). For read-only endpoints, consider a simpler key. Always use HTTPS to prevent key exposure. Never log the key.
Also consider rate limiting by API key. Without it, a compromised key or abusive client can overwhelm your server. A simple in-memory counter per key works for single-server deployments.
Another nuance: key rotation. In production, you'll need to expire old keys without downtime. Include an expiration timestamp in your key storage and allow a grace period.
Always use hash_equals() for key comparison — it prevents timing attacks. Never use == or === which can leak the key length and content via timing differences.
Production Insight
We once saw a team store the API key in the same git repo as the code.
Cue a security audit finding the key in GitHub history — had to rotate all keys.
Rule: store keys in environment variables or a .env file outside version control.
Key Takeaway
Authenticate with a simple header check — no JWT needed for internal APIs.
Use hash_equals() to prevent timing attacks.
Keep keys out of code — use environment variables.
Authentication Approach Decision
IfInternal API, trusted clients only
→
UseStatic API key in env variable — simple and sufficient.
IfPublic API with many clients
→
UseUse JWT or OAuth2 — enables scoped access and revocation.
Database Integration with PDO
Pure PHP connects to databases via PDO — a consistent interface for MySQL, PostgreSQL, and others. Create a PDO instance once and pass it to handlers. Use prepared statements to prevent SQL injection. Group database operations inside transaction blocks for atomicity.
For read-heavy endpoints, consider caching query results with APCu or Redis. PDO doesn't cache by default, so repeated identical queries hit the database every time.
Also: when using transactions, remember that PDO auto-commits by default. You must explicitly beginTransaction() and handle commit/rollback. A missing commit after a series of inserts can leave partial data.
Use a singleton or dependency container to ensure only one PDO connection per request. Creating a new connection on every handler call will exhaust MySQL connections quickly.
Production Insight
A common failure: PDO exceptions not caught in API handlers.
If a database query fails, the API returns a 500 with a default error message, but sometimes PDO exposes the query in the error log (if display_errors is on).
Rule: always catch PDOException, log it, and return a generic 500 response.
Key Takeaway
PDO is your only ORM-less friend — use prepared statements.
Create connection once, reuse across handlers.
Catch database exceptions to avoid leaking internal error details.
Database Connection Management
IfSingle server, low concurrency
→
UseCreate a new PDO connection per request — simple and safe.
IfHigh concurrency or multiple servers
→
UseUse a persistent connection or connection pool via PDO::ATTR_PERSISTENT with caution — or move to a dedicated pool library.
Middleware: CORS, Request Logging, and Input Sanitization
In pure PHP, middleware is just code that runs before your route handler. Common middleware tasks include setting CORS headers, logging requests, and sanitizing input. You'll often need to handle a middleware chain where each step can exit early or modify the request data.
The biggest pitfall: PHP's php://input stream can only be consumed once. If your logging middleware reads the raw body, your route handler will get an empty string unless you pass the parsed data forward. Use a static class variable or a dependency container to share the decoded request body across the request lifecycle.
Also: timing attacks are real. Logging the exact timestamp of each request can reveal information about processing times. Avoid logging sensitive delay data.
If you read php://input in middleware for logging, the route handler will get an empty body. Always cache the parsed body in a static property like above.
Production Insight
CORS headers must be set before any output. If you echo something by accident or include a file that outputs whitespace, the headers fail silently.
Rule: place header() calls at the very top of your entry point, before any includes or logic.
Also, logging the raw request body can leak PII — always mask sensitive fields before writing to logs.
Key Takeaway
Middleware in pure PHP is a series of function calls before the route.
Handle CORS early, parse body once, and never log raw credentials.
A middleware chain makes your API testable and maintainable.
Middleware Ordering Decision
IfCORS errors in browser
→
UsePut CORS middleware first — before any routing or output.
IfNeed to log request body for debugging
→
UseUse parseBodyOnce() to capture body in middleware, then pass it to handler via static cache.
IfUser input contains HTML/JavaScript
→
UseApply sanitize() middleware before storing or outputting data.
Implementing Rate Limiting and Basic Security
Without a framework, you need to implement rate limiting yourself to prevent abuse. A simple approach: track request counts per API key in a file or shared memory (APCu). Use a sliding window or token bucket algorithm. For single-server deployments, a file-based counter works. For distributed systems, move to Redis.
Beyond rate limiting, enforce basic security: always sanitize output with htmlspecialchars if you ever return HTML, use prepared statements for every database query, and validate that incoming data matches expected types. Never trust client input. Use environment variables for secrets — never hardcode them.
Also: consider using Content Security Policy headers to prevent XSS if your API serves any HTML. Even though it's a JSON API, future endpoints might serve user-generated content in responses.
File-based rate limiting won't work across multiple servers. For scalable production, use Redis with INCR and EXPIRE. This example is for single-server or dev environments only.
Production Insight
A missing rate limit can lead to accidental DDoS from a rogue client or misconfigured webhook.
We once saw a client retry a POST request 10,000 times in 5 seconds, bringing down the database.
Rule: always have a per-IP and per-key rate limit, even if low, in production.
Key Takeaway
Rate limiting is not optional in production — even a simple file-based limiter buys you time.
Always sanitize output (htmlspecialchars) and use prepared statements.
Security is a process, not a single feature.
Rate Limiting Storage Decision
IfSingle server, low traffic
→
UseFile-based counter — simple, no external dependency.
IfMultiple servers or high traffic
→
UseUse Redis with atomic INCR and TTL — consistent across nodes.
IfAlready using Redis for caching
→
UseReuse Redis connection for rate limiting — one less dependency.
Testing Your Pure PHP REST API
You can't ship an API you haven't tested. For pure PHP, set up a test suite using PHPUnit that starts PHP's built-in server, runs requests against it, and asserts responses. This catches regression before they hit production. Key: start the server with php -S localhost:8000 -t public/ and wait for the socket to open.
For unit tests, mock the request globals using a test double. But integration tests that hit real HTTP endpoints are more reliable — they verify routing, headers, and body parsing end-to-end. Use curl or Guzzle in your test cases.
Also: test for edge cases like missing headers, empty body, malformed JSON, and large payloads. These are often the silent killers in production.
Use process isolation if you mock superglobals. But integration tests with a real server are more reliable — they catch header and router bugs that unit tests miss.
Production Insight
Integration tests that use the built-in server can fail due to port conflicts if another test process is running.
Use a random port or a lock file to avoid collisions. We lost 2 hours debugging a test that failed only on CI because port 8000 was occupied.
Rule: allocate ports dynamically or use a dedicated CI environment.
Key Takeaway
Test your API with real HTTP requests, not just mocked superglobals.
Start a PHP built-in server in test setup and hit it with curl or Guzzle.
Catch the 415 and 400 errors before your mobile client does.
Testing Approach Decision
IfYou need fast feedback during development
→
UseWrite unit tests with mocked $_SERVER and php://input — fast but may miss integration issues.
IfYou want to catch real HTTP bugs before production
→
UseWrite integration tests that start the built-in server — slower but catches routing, headers, and body parsing.
Deploying Your Pure PHP REST API
Development server (php -S) is fine for testing, but production requires PHP-FPM and a web server like Nginx. The built-in server is single-threaded and not designed for concurrent requests.
Configure Nginx to proxy requests to PHP-FPM. Use environment variables for database credentials and API keys. Enable OPCache for performance and set error logging to files.
A common pitfall: forgetting to set document root correctly, leading to 404 errors for all routes. Nginx must point to your public directory and route everything to index.php.
Also: ensure your PHP-FPM pool settings are tuned for your traffic. The default pm.max_children is often too low for moderate traffic.
Use a Dockerfile with php:8.3-fpm and nginx:alpine for consistent deployments. Combine with a docker-compose.yml for local development.
Production Insight
The most common deployment failure is forgetting to disable display_errors in php.ini.
Stack traces from uncaught exceptions leak database credentials and file paths.
Rule: always set display_errors = 0 and log_errors = 1 in production.
Key Takeaway
Never use the built-in PHP server in production.
Deploy with PHP-FPM + Nginx for concurrency and isolation.
Always disable display_errors and manually handle exceptions.
Deployment Strategy Decision
IfLow traffic internal API (<100 req/s)
→
UseUse PHP built-in server behind a reverse proxy (nginx) for simplicity.
IfHigh traffic public API (>100 req/s)
→
UseUse PHP-FPM with process tuning (pm.max_children, pm.start_servers) and OPCache.
Remote Debugging Without Xdebug: Live Log Injection
You deployed to production and the API returns a 500. No stack trace. No access to error logs. That's when you learn to inject structured debug data into your response headers without breaking the JSON contract. Set a custom header like X-Debug-Log only when an internal flag is active. Use error_get_last() and serialize it into the header value. This works because HTTP headers can carry small payloads without corrupting the body. Never ship this to customers — it's a kill switch for your eyes only. The WHY: You cannot attach a debugger to a live production server, but you can tag every response with breadcrumbs. The HOW: Check a $_ENV['DEBUG_MODE'] flag at the entry point, capture errors in a shutdown handler, and append them as a comma-delimited string to the header. This saved my team's Saturday three times last year.
index.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge
$isDebug = ($_ENV['DEBUG_MODE'] ?? 'false') === 'true';
register_shutdown_function(function () use ($isDebug) {
if (!$isDebug) return;
$error = error_get_last();
if ($error !== null) {
// Prevents body corruptionheader('X-Debug-Log: ' . urlencode(json_encode($error)));
}
});
// Example route that triggers a fatal
$_SERVER['REQUEST_URI'] = '/articles/1';
echojson_encode(['status' => 'hello']);
Never leave DEBUG_MODE on in production config. One engineer did. An attacker sent malformed input, the debug header leaked internal file paths, and the breach was game over. Toggle via environment, not code.
Key Takeaway
Inject debug data into custom HTTP headers to trace production errors without modifying your response JSON.
Auto-Generate API Docs From Your Router Table
You have ten endpoints. No documentation. The frontend team sends wrong payloads. You fix it every sprint. Stop. Pull the route metadata directly from your PHP router at runtime. Define each route as an array of method, path, required fields, and a one-line description. When a request hits /docs.json, loop that array and return it as JSON. The WHY: Documentation rots the second you write it by hand. Generated docs always match reality because they read the same array that drives routing. The HOW: Store your routes in a static config file. Add a DOCS_ENABLED flag in your .env. Expose a GET /docs endpoint that returns the table. No external tool. No parsing annotations. This approach also lets you build a Postman collection generator later with zero refactoring.
{"method":"GET","path":"/users","fields":["limit","offset"],"desc":"List users with pagination"},
{"method":"POST","path":"/users","fields":["name","email","password"],"desc":"Create a new user"}
]
Dev Tip:
Prepend the docs endpoint with a random token to keep it hidden from scrapers. Example: /docs/e7a3f2.json. Hard-code the token in your deployment env file.
Key Takeaway
Generate your API documentation from the same route array that powers your router so it never goes stale.
● Production incidentPOST-MORTEMseverity: high
The Missing Content-Type Header That Killed a Mobile App
Symptom
Mobile app sends POST request with JSON body. Server returns HTTP 200 OK but response body is empty.
Assumption
The developer assumed PHP automatically recognizes JSON payloads and populates $_POST.
Root cause
PHP only populates $_POST automatically for application/x-www-form-urlencoded or multipart/form-data requests. For JSON, the raw body must be read via php://input. The response was empty because the server processed no data and returned nothing.
Fix
Read the request body with file_get_contents('php://input') and decode with json_decode(). Set response Content-Type header to application/json and always include a meaningful response body.
Key lesson
Never assume request body parsing — always check Content-Type header and read raw input for JSON.
Always return a JSON body even on success, with at least a status field.
Add early validation: if expected Content-Type is mismatched, reject with 415 Unsupported Media Type.
Log incoming request headers during development to catch these mismatches immediately.
Production debug guideSymptom → Action guide for common failures4 entries
Symptom · 01
API returns 404 for valid routes
→
Fix
Check router logic: are you parsing REQUEST_URI with full path or only the script name? Use parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) to strip query strings. Log the parsed route to confirm.
Symptom · 02
POST/PUT request returns 200 but data not saved
→
Fix
Check if you're reading the body. Use file_get_contents('php://input') and verify Content-Type header matches your parser (application/json). If using $_POST, switch to raw input.
Symptom · 03
API returns empty JSON object on error
→
Fix
You're likely not sending an error response. After catch block, always http_response_code(500) and echo json_encode(['error' => 'Internal Server Error']). Enable PHP error reporting during dev: error_reporting(E_ALL); ini_set('display_errors', 1).
Symptom · 04
CORS error in browser when calling API from frontend
→
Fix
Add CORS headers in a bootstrap file: header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization'); Handle OPTIONS preflight requests early.
★ Pure PHP REST API Debugging Cheat SheetQuick commands and fixes for the most common production issues.
Empty response body−
Immediate action
Check response headers via curl -v
Commands
curl -v -X POST http://your-api/resource -H 'Content-Type: application/json' -d '{"key":"value"}'
tail -f /var/log/php_errors.log
Fix now
Ensure you echo json_encode($response) after headers. Add header('Content-Type: application/json'); before output.
Wrap your route handler in try-catch, log exception with error_log($e->getMessage()), and return 500 with generic message.
PUT/DELETE not recognized+
Immediate action
Verify the method is sent correctly
Commands
echo $_SERVER['REQUEST_METHOD']; die;
Check client library — some only support GET/POST by default
Fix now
Add a simple method override: if (isset($_SERVER['HTTP_X-HTTP-Method-Override'])) { $_SERVER['REQUEST_METHOD'] = $_SERVER['HTTP_X-HTTP-Method-Override']; }
Pure PHP vs Framework Approaches
Aspect
Pure PHP
Laravel / Symfony
Routing
Manual regex or if/else
Declarative routes.php / annotations
Request Parsing
Read php://input, validate manually
Request object with built-in JSON parsing
Validation
Custom validation classes
FormRequest / Validator with rules
Authentication
Manual header check with hash_equals
Guards, Sanctum, Passport
Database
Raw PDO with prepared statements
Eloquent ORM with relationships
Error Handling
Custom try-catch and response building
Exception handler with debug page
Learning Curve
Steeper at start, deeper understanding
Shorter ramp-up, but magic hides details
Common mistakes to avoid
7 patterns
×
Using $_POST for JSON requests
Symptom
API returns 200 OK but data fields are empty or missing.
Fix
Read raw body with file_get_contents('php://input'), decode with json_decode(), and parse parameters from the resulting array instead of relying on $_POST.
×
Not setting Content-Type header on responses
Symptom
Mobile apps or TypeScript fetch() receive an empty response or fail to parse body.
Fix
Always set header('Content-Type: application/json'); before echoing output. Place this in a central bootstrap file or response helper.
Store credentials in environment variables or a .env file. Use getenv() or $_ENV in code. Add .env to .gitignore.
×
Skipping input validation for all fields
Symptom
Invalid data enters database, causing crashes or security issues. SQL injection possible if not using prepared statements.
Fix
Validate every incoming field. Use filter_var, regex, or a custom validator class. Use PDO prepared statements to prevent injection.
×
Exposing detailed error messages in production
Symptom
Stack traces and SQL queries appear in JSON response under 500 errors.
Fix
In production, set display_errors = 0, log errors with error_log(), and return a generic 'Internal Server Error' with status 500.
×
Logging the full request body without sanitization
Symptom
Sensitive data (passwords, API keys) appears in log files accessible to other teams.
Fix
Mask sensitive fields before logging, e.g., replace 'password' field with '***'. Only log what's necessary for debugging.
×
Not handling CORS preflight OPTIONS requests
Symptom
Browser shows CORS error even after setting headers, especially for non-simple requests with custom headers.
Fix
In your entry point, check for OPTIONS method early, set appropriate CORS headers, and return 204 with no body. Do this before any routing logic.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What is the difference between $_POST and php://input?
Q02SENIOR
How would you handle CORS preflight requests in a pure PHP API?
Q03SENIOR
Describe a production incident where a missing Content-Type header cause...
Q04SENIOR
How do you implement rate limiting without a framework? What are the tra...
Q05SENIOR
Explain how to read JSON request body in pure PHP and why $_POST doesn't...
Q01 of 05JUNIOR
What is the difference between $_POST and php://input?
ANSWER
$_POST is pre-populated by PHP only for form-urlencoded or multipart form data. For JSON payloads, PHP does not populate $_POST; you must read the raw body using file_get_contents('php://input'). This is a common source of empty responses in pure PHP APIs.
Q02 of 05SENIOR
How would you handle CORS preflight requests in a pure PHP API?
ANSWER
In the entry point (e.g., index.php), check for OPTIONS method early using $_SERVER['REQUEST_METHOD']. If it's OPTIONS, set the appropriate CORS headers (Access-Control-Allow-Origin, Methods, Headers) and return a 204 response with no body. Do this before any routing logic. Also ensure all responses carry the CORS headers.
Q03 of 05SENIOR
Describe a production incident where a missing Content-Type header caused an empty response. How would you prevent it?
ANSWER
A mobile client sent JSON without the Content-Type: application/json header. The server's RequestHandler skipped JSON parsing (since $_POST was empty), returned a 200 with no body. The app showed a blank screen. To prevent it, always validate the Content-Type header in the request parser, reject with 415 if missing or mismatched, and log incoming headers during development. Also, always return a response body with at least a status field.
Q04 of 05SENIOR
How do you implement rate limiting without a framework? What are the trade-offs?
ANSWER
Track request counts per client identifier (API key or IP) using a sliding window or token bucket. For single-server, use a file-based counter or APCu. For distributed, use Redis with INCR and EXPIRE. Trade-offs: file-based is simple but not concurrency-safe across processes; Redis is consistent but adds latency and a dependency. Always apply per-IP and per-key limits.
Q05 of 05SENIOR
Explain how to read JSON request body in pure PHP and why $_POST doesn't work.
ANSWER
$_POST is only populated by PHP for application/x-www-form-urlencoded or multipart/form-data. JSON is sent as a raw stream. Use file_get_contents('php://input') to read the raw body, then json_decode() to parse it into an array. Always check the Content-Type header to ensure you're receiving JSON. Also note that php://input can only be read once per request.
01
What is the difference between $_POST and php://input?
JUNIOR
02
How would you handle CORS preflight requests in a pure PHP API?
SENIOR
03
Describe a production incident where a missing Content-Type header caused an empty response. How would you prevent it?
SENIOR
04
How do you implement rate limiting without a framework? What are the trade-offs?
SENIOR
05
Explain how to read JSON request body in pure PHP and why $_POST doesn't work.