Resource
The thing you are working with, such as users, campaigns, orders, reports, or products.
REST API Guide
A single consolidated guide to REST APIs: core concepts, intermediate production patterns, advanced architecture, MySQL and JSON integration, reference labs across languages, and troubleshooting challenges. Jump to any section using the pills below.
REST is an architectural style. The main idea is not "return JSON", but to design systems around resources, standard HTTP semantics, stateless requests, cacheable responses, and intermediaries that can improve scalability and security.
Many real APIs are HTTP resource APIs that follow REST strongly without implementing full hypermedia navigation everywhere. That is common in production. What matters most is consistent resource design and correct HTTP behavior.
The thing you are working with, such as users, campaigns, orders, reports, or products.
The serialized form of resource state, usually JSON, labeled by a media type such as `application/json`.
The URL that represents a resource or action, such as `/api/campaigns` or `/api/orders/17`.
The HTTP verb that tells the server what you want to do: GET, POST, PUT, PATCH, or DELETE.
Each request should contain enough context to be processed correctly without relying on hidden server-side session state.
Clients use consistent HTTP semantics instead of custom remote method names for every operation.
Metadata about the call such as content type, authorization token, request id, or API version.
The JSON payload sent with POST, PUT, or PATCH when you create or update data.
The JSON and status code returned by the server after it processes the request.
| Method | Example endpoint | Meaning |
|---|---|---|
| GET | /api/articles |
Read a list of articles |
| GET | /api/articles/25 |
Read one article |
| POST | /api/articles |
Create a new article |
| PUT | /api/articles/25 |
Replace the entire article |
| PATCH | /api/articles/25 |
Update one or two fields only |
| DELETE | /api/articles/25 |
Delete the article |
| Code | Meaning |
|---|---|
| 200 OK | The request worked and the server returned data. |
| 201 Created | A new record was created successfully. |
| 204 No Content | The request worked, but there is no body to return. |
| 400 Bad Request | The input was wrong or missing required fields. |
| 401 Unauthorized | Auth is missing or invalid. |
| 403 Forbidden | The user is authenticated but not allowed to do this action. |
| 404 Not Found | The endpoint or record does not exist. |
| 409 Conflict | There is a duplicate or version conflict. |
| 429 Too Many Requests | The client hit the rate limit. |
| 500 Internal Server Error | The server failed while processing the request. |
A website wants to save a lead form submission. The frontend sends a `POST` request to `/api/leads`. The backend validates the fields, stores the row in a database, and returns JSON with the newly created record.
curl -X POST https://example.com/api/leads \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"name": "Asha",
"email": "asha@example.com",
"source": "landing-page"
}'
{
"success": true,
"data": {
"id": 101,
"name": "Asha",
"email": "asha@example.com",
"source": "landing-page",
"created_at": "2026-03-25T10:45:00Z"
}
}
Use bearer tokens, API keys, or signed requests so only approved clients can access data.
Split large lists into pages or cursor windows so responses stay fast and predictable.
Allow clients to ask for exactly the records they need instead of returning everything.
Reject missing or malformed input before it reaches the database.
Keep `/v1` or header-based versions for breaking changes.
Use ETag, Cache-Control, or CDN caching on safe GET endpoints.
Protect side-effecting retries with idempotency keys so duplicate charges, orders, or events are not created.
Use consistent error payloads instead of bespoke error shapes per endpoint.
{
"success": true,
"data": [
{ "id": 1, "title": "Campaign A", "status": "active" },
{ "id": 2, "title": "Campaign B", "status": "paused" }
],
"pagination": {
"page": 1,
"limit": 2,
"total": 18,
"pages": 9
}
}
{
"type": "https://example.com/problems/validation-error",
"title": "Validation failed",
"status": 400,
"detail": "name is required",
"instance": "/v1/items"
}
This is the point where REST meets the website UI. The browser calls the endpoint, gets JSON, and re-renders cards, tables, charts, or forms.
async function loadCampaigns(page = 1) {
const response = await fetch(`/pages/api.php?table=blogs&limit=5&page=${page}&key=YOUR_KEY`, {
headers: {
"Accept": "application/json",
"X-API-Version": "1.0"
}
});
if (!response.ok) {
throw new Error(`Request failed with ${response.status}`);
}
const data = await response.json();
renderCampaigns(data);
}
Use an `Idempotency-Key` when the same create request might be retried. This prevents duplicate orders, payments, or conversions.
Retry only safe failures such as timeouts or transient 5xx errors, and always back off instead of retrying instantly.
Protect shared infrastructure with per-key, per-IP, or per-user request limits.
Prevent lost updates with row version fields, timestamps, or optimistic locking.
Attach request ids, trace ids, latency, and error tags so teams can debug distributed systems quickly.
Sign webhook payloads with HMAC and verify the signature before trusting the event.
Choose REST, GraphQL, gRPC, async events, or streaming based on public compatibility, flexibility, and performance needs.
If a client times out, it may retry even though the server already created the record. Idempotency lets the server recognize that duplicate attempt and return the original result instead of creating a second row.
POST /api/orders
Idempotency-Key: order-20260325-00091
Content-Type: application/json
Authorization: Bearer TOKEN
{
"customer_id": 44,
"amount": 199.00,
"currency": "USD"
}
REST is request-response, but production systems often pair it with webhooks so downstream systems get updates as soon as something changes.
$payload = file_get_contents('php://input');
$incomingSignature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expectedSignature = hash_hmac('sha256', $payload, $sharedSecret);
if (!hash_equals($expectedSignature, $incomingSignature)) {
http_response_code(401);
exit('Invalid signature');
}
When multiple services call each other, a request id or trace id is what lets you connect the browser error to the backend logs, queue message, and database write.
{
"request_id": "req_74b2",
"trace_id": "trace_98ee",
"status": "error",
"error_code": "upstream_timeout",
"latency_ms": 1432
}
A fast REST endpoint can accept the request, validate it, store a job, and return immediately while background workers do the heavier processing.
High-read endpoints often sit behind CDN or Redis caching layers so the origin database is not hit for every dashboard load.
If the UI needs push updates instead of periodic fetch calls, REST usually handles CRUD while websockets or SSE handle live notifications.
Central place for auth, TLS termination, rate limits, routing, and policy enforcement.
Backend-for-Frontend keeps mobile, web, and partner clients from forcing one generic API shape.
Distributed workflows use local transactions plus compensating steps instead of one global transaction.
Moves mTLS, retries, telemetry, and traffic control into the proxy layer instead of every app.
| Style | Typical contract | Strength | Best fit |
|---|---|---|---|
| REST | OpenAPI plus HTTP semantics | Strong HTTP caching and public interoperability | Public APIs and broad platform support |
| GraphQL | Schema and client-selected operations | Harder to cache at the CDN edge | Client-driven data graphs |
| gRPC | Protobuf service definition | Usually internal and performance-oriented | Inter-service calls and streaming RPC |
A common production pattern is REST for commands and queries, then async events for propagation. Think `OrderCreated`, `PaymentCaptured`, or `ItemUpdated` flowing through queues or brokers.
Use SSE for simple server push over HTTP and WebSockets when you need full-duplex communication or bidirectional live interaction.
Public API products often combine docs, usage plans, API keys, quotas, billing, and self-serve portals. Developer experience becomes part of the product itself.
Sends `fetch()` requests and renders JSON into cards, tables, charts, or form messages.
Receives the request, validates it, runs SQL, and returns JSON with status codes.
Stores the records. The API reads and writes rows using SQL and converts the result set into JSON.
CREATE TABLE articles (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
body TEXT NOT NULL,
status ENUM('draft','published') DEFAULT 'draft',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
);
JSON columns are useful when some fields are flexible or nested. They let you store structured metadata while still updating only the path that changed.
-- Extract a JSON value SELECT JSON_EXTRACT(metadata, '$.specs.mp') AS mp FROM items WHERE id = 1; -- Update nested JSON fields UPDATE items SET metadata = JSON_SET(metadata, '$.flags.featured', true, '$.color', 'red') WHERE id = 1;
When one API call changes multiple columns or tables, use a transaction so the write fully succeeds or fully rolls back.
START TRANSACTION; UPDATE items SET name = 'Camera' WHERE id = 1; UPDATE items SET metadata = JSON_SET(metadata, '$.updatedBy', 'api') WHERE id = 1; COMMIT;
This is the classic "load data into the website" endpoint. It queries MySQL, fetches rows, and returns JSON.
<?php
header('Content-Type: application/json; charset=utf-8');
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->query('SELECT id, title, status, updated_at FROM articles ORDER BY id DESC LIMIT 10');
$rows = $stmt->fetchAll();
echo json_encode([
'success' => true,
'data' => $rows,
], JSON_UNESCAPED_UNICODE);
This creates a new row from a JSON body sent by a form, admin panel, or another service.
<?php
$payload = json_decode(file_get_contents('php://input'), true);
$stmt = $pdo->prepare('INSERT INTO articles (title, body, status) VALUES (:title, :body, :status)');
$stmt->execute([
':title' => $payload['title'],
':body' => $payload['body'],
':status' => $payload['status'] ?? 'draft',
]);
echo json_encode([
'success' => true,
'insert_id' => $pdo->lastInsertId(),
]);
async function loadArticles() {
const response = await fetch('/api/articles');
const payload = await response.json();
const container = document.querySelector('#article-list');
container.innerHTML = payload.data.map((item) => `
<article class="article-card">
<h3>${item.title}</h3>
<p>Status: ${item.status}</p>
</article>
`).join('');
}
loadArticles();
async function saveArticle(id, updates) {
const response = await fetch(`/api/articles/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
});
if (!response.ok) {
throw new Error(`Update failed with ${response.status}`);
}
await loadArticles();
}
REST itself is request-response, so the simplest near real-time website update is short polling.
loadArticles(); setInterval(loadArticles, 30000);
/pages/api.php shows MySQL-backed JSON output, auth checks, and table querying./api/api-example.php shows CRUD against a JSON file, useful for understanding request flow before adding MySQL./api/api-webhook.php shows how updates can trigger webhook events after data changes.POST /oauth/token - Demo auth endpoint that returns a bearer token for tutorials and lab flows.POST /v1/items - Create an item with a JSON metadata payload.GET /v1/items?limit=...&cursor=... - Cursor pagination pattern for stable list traversal.GET /v1/items/{id} - Read a single item, often with attachments or related records.PATCH /v1/items/{id} - Partial update using merge-style behavior.PATCH /v1/items/{id}/metadata - JSON_SET style update for nested JSON fields.POST /v1/items/{id}/attachments - Multipart upload flow.GET /v1/stream/items - SSE stream for near real-time stats or event updates./ws - WebSocket endpoint for echo or broadcast patterns.Express plus Sequelize works well for classic HTTP resource APIs, file uploads, and JSON column updates.
import express from "express";
import multer from "multer";
const app = express();
app.use(express.json());
const upload = multer({ dest: "uploads/" });
app.post("/v1/items", requireAuth, async (req, res) => {
const item = await Item.create({ name: req.body.name, metadata: req.body.metadata || {} });
res.status(201).json(item);
});
app.patch("/v1/items/:id/metadata", requireAuth, async (req, res) => {
await sequelize.query(
"UPDATE items SET metadata = JSON_SET(metadata, ?, CAST(? AS JSON)) WHERE id = ?",
{ replacements: [req.body.path, JSON.stringify(req.body.value), Number(req.params.id)] }
);
res.json(await Item.findByPk(req.params.id));
});
app.get("/v1/stream/items", requireAuth, async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
const timer = setInterval(async () => {
res.write(`data: ${JSON.stringify({ count: await Item.count() })}\n\n`);
}, 1000);
req.on("close", () => clearInterval(timer));
});
FastAPI fits strongly typed Python services, SQLAlchemy sessions, and built-in WebSocket support.
@app.post("/v1/items", dependencies=[Depends(require_user)])
def create_item(payload: dict, db: Session = Depends(get_db)):
item = Item(name=payload["name"], metadata=payload.get("metadata", {}))
db.add(item)
db.commit()
db.refresh(item)
return item
@app.patch("/v1/items/{item_id}/metadata", dependencies=[Depends(require_user)])
def json_set(item_id: int, payload: dict, db: Session = Depends(get_db)):
db.execute(
text("UPDATE items SET metadata = JSON_SET(metadata, :path, CAST(:value AS JSON)) WHERE id = :id"),
{"path": payload["path"], "value": json.dumps(payload["value"]), "id": item_id},
)
db.commit()
return db.get(Item, item_id)
@app.websocket("/ws")
async def ws(websocket: WebSocket):
await websocket.accept()
while True:
msg = await websocket.receive_text()
await websocket.send_text(json.dumps({"type": "echo", "msg": msg}))
Laravel keeps migrations, model casting, validation, auth, and file handling in one familiar stack.
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->string('name', 190);
$table->json('metadata');
$table->timestamps();
});
class Item extends Model {
protected $fillable = ['name', 'metadata'];
protected $casts = ['metadata' => 'array'];
}
class ItemController extends Controller {
public function update(Request $request, int $id) {
$item = Item::findOrFail($id);
$patch = $request->input('metadataPatch', []);
$item->metadata = array_merge($item->metadata ?? [], $patch);
if ($request->has('name')) $item->name = $request->input('name');
$item->save();
return $item;
}
}
Use curl first when debugging. It removes browser complexity and lets you verify the exact request shape quickly.
# Create item
curl -s http://localhost:3000/v1/items \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Camera","metadata":{"brand":"Sony","specs":{"mp":24}}}'
# JSON_SET patch
curl -s http://localhost:3000/v1/items/1/metadata \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"path":"$.flags.featured","value":true}'
openapi: 3.1.0
info:
title: Items API
version: 1.0.0
paths:
/v1/items:
get:
summary: List items (cursor pagination)
post:
summary: Create item
/v1/items/{id}/metadata:
patch:
summary: Update JSON path via JSON_SET
| Problem | What you see | Reason | Fix |
|---|---|---|---|
| CORS blocked | Browser request fails before reaching the API | Missing `Access-Control-Allow-Origin` or bad preflight config | Return the correct CORS headers and handle `OPTIONS` requests |
| 401 Unauthorized | Token or API key is invalid, missing, or expired | Client sent wrong auth header or stale token | Refresh token, check scopes, and verify header name |
| 404 Not Found | Wrong endpoint path or missing record id | Route mismatch or bad URL construction | Check route naming and confirm the resource exists |
| 409 Conflict | Two updates clash or a duplicate key already exists | Concurrent writes or unique constraint violation | Re-fetch latest state and retry safely |
| 429 Too Many Requests | The API is protecting itself from too many calls | Client loop, burst traffic, or missing backoff | Respect rate-limit headers and add retry delay |
| 500 Internal Server Error | The server failed while processing | Unhandled exception, SQL issue, or null data path | Inspect logs, request id, stack trace, and database errors |
| Invalid JSON | Body cannot be parsed | Broken commas, quotes, or content type mismatch | Validate body before sending and set `Content-Type: application/json` |
| Stale data on page | The UI shows old values after save | Cache layer, no re-fetch, or race condition | Invalidate cache and reload the relevant data source |
| Slow response | Users wait too long for data | Heavy SQL, no index, or large payload | Add indexes, reduce fields, paginate, and cache safe reads |
| Duplicate events | Same webhook or POST is processed twice | Client retry without idempotency | Use idempotency keys and dedupe on event id |
| BOLA / object access issue | A user can access another user record by changing the ID | Object-level authorization is missing | Check ownership or scope on every object lookup |
| CORS too open | Any origin can call the API in unsafe ways | Allow-all policy was left enabled | Restrict origins, methods, and credential behavior deliberately |
Use idempotency keys so retries return the stored first result instead of double-creating charges or money movement.
Use link-based or token-based pagination when datasets are large and offset pagination becomes unstable or slow.
AdTech APIs sometimes use `204 No Content` to signal a valid no-bid or empty response path instead of treating it as an error.
For multi-step workflows across services, use compensating steps instead of trying to force one distributed transaction everywhere.
Enter any two values
to calculate the third
More tools coming soon