An Application Programming Interface (API) is a formalized set of rules, contracts, and communication protocols that enables disparate software systems to exchange data and invoke functionality without exposing private internal source code or database implementations.
In local programming, an API refers to exported classes and methods (e.g., the JavaScript Math.max() API). In full-stack web development, an API refers to Web Services: network-accessible endpoints operating over standard internet protocols (HTTP/HTTPS) utilizing structured, language-agnostic data payloads (principally JSON or XML).
Web APIs act as the universal abstraction glue:
Coined by Roy Fielding in his 2000 doctoral dissertation, REST (Representational State Transfer) is not a standard, protocol, or software library; it is an architectural style for distributed hypermedia systems. To be considered genuinely "RESTful", an API must adhere to six fundamental design constraints.
Cache-Control and ETag headers) to eliminate redundant network roundtrips./api/nodes/12), manipulated through representations (JSON), and accompanied by self-descriptive MIME types.| Protocol / Architecture | Data Format | Transport Protocol | Caching Support | Primary Strengths & Trade-offs |
|---|---|---|---|---|
| REST | Primarily JSON (also XML/HTML) | Exclusively HTTP/HTTPS | Native HTTP Caching (ETags, Max-Age) | Universal industry standard; high caching performance; occasional over-fetching. |
| GraphQL | JSON | HTTP POST (Single endpoint) | Complex (requires client-side normalized caching) | Solves over/under-fetching; client queries exact fields needed; high server schema complexity. |
| gRPC | Protobuf (Binary Protocol Buffers) | HTTP/2 (Multiplexed Streams) | Application-level | Blazing fast serialization; strictly typed; ideal for internal microservices; browser friction. |
In RESTful design, URLs target nouns (resources), and HTTP verbs define the action (intent) to perform upon that resource.
| HTTP Verb | CRUD Action | Safe? | Idempotent? | Request Body? | Semantic Behavior |
|---|---|---|---|---|---|
GET |
Read | Yes | Yes | No | Fetches resource representation. Never mutates server data. |
POST |
Create | No | No | Yes | Submits payload to create a new subordinate resource. Submitting twice creates 2 entities! |
PUT |
Replace | No | Yes | Yes | Completely replaces the target resource with the uploaded payload. |
PATCH |
Modify | No | No | Yes | Applies partial mutations to specific fields of an existing resource. |
DELETE |
Delete | No | Yes | Optional | Destroys the specified resource. Running 10 times leaves resource deleted. |
A well-designed REST API communicates success, redirection, and errors using standardized 3-digit HTTP status codes. Returning 200 OK with an embedded { "error": true } JSON payload is a severe architectural anti-pattern.
101 Switching Protocols for WebSockets).200 OK, 201 Created, 204 No Content).301 Moved Permanently, 304 Not Modified for caching).400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests).500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable).Modern frontend applications consume backend web services using asynchronous HTTP client interfaces. Two primary choices dominate the industry: the browser's native Fetch API and the third-party Axios client library.
The native fetch() promise does NOT reject on HTTP error statuses (like 404 or 500)! It only rejects when a network failure occurs or the request was blocked by the browser (DNS failure, CORS violation, or aborted request).
Developers must manually inspect response.ok (which is true for 200-299 status codes):
const res = await fetch('/api/nodes');
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`); // Mandatory check!
By contrast, Axios automatically rejects its promise whenever an HTTP status outside the 2xx range is returned.
// --- Option A: Production Native Fetch with Timeout & Guard ---
export async function fetchTelemetryNodes() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5-second timeout
try {
const res = await fetch('https://api.devascent.me/v1/nodes', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal
});
// Mandatory: Check response.ok status
if (!res.ok) {
const errorPayload = await res.json();
throw new Error(errorPayload.message || `Request failed with status ${res.status}`);
}
return await res.json();
} finally {
clearTimeout(timeoutId);
}
}
// --- Option B: Production Axios Client with Global Interceptors ---
import axios from 'axios';
export const apiClient = axios.create({
baseURL: 'https://api.devascent.me/v1',
timeout: 5000,
headers: { 'Content-Type': 'application/json' }
});
// Request Interceptor: Automatically injects JWT
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response Interceptor: Catches 401s globally
apiClient.interceptors.response.use(
(response) => response.data, // Unwraps data payload automatically
(error) => {
if (error.response?.status === 401) {
console.warn('Session expired. Redirecting to login...');
window.location.href = '/login';
}
return Promise.reject(error.response?.data || error);
}
);
Production APIs must protect backend database servers from resource exhaustion by enforcing Pagination (never returning 1,000,000 rows in a single query) and Rate Limiting (preventing abusive clients from flooding endpoints).
?page=2&limit=20): Standard and easy to implement using SQL LIMIT 20 OFFSET 20. However, it suffers from performance degradation at deep pages (e.g., OFFSET 1000000 forces the database to read and discard 1 million rows) and produces duplicate records if items are inserted while browsing.?after=c_1042): Feeds the client a pointer (usually an encoded timestamp or primary key) of the last retrieved item. The database query uses WHERE id > $cursor LIMIT 20, which hits an index in O(1) time regardless of how deep the client paginates!import express from 'express';
import rateLimit from 'express-rate-limit';
const router = express.Router();
// Rate Limiting Guard: Max 100 requests per 15-minute window per IP
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true, // Returns standard 'RateLimit-*' headers
legacyHeaders: false,
message: { error: 'Too many requests. Try again in 15 minutes.' }
});
router.get('/telemetry', apiLimiter, async (req, res) => {
// Sanitize pagination parameters
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit, 10) || 20));
const offset = (page - 1) * limit;
// Simulated database response with metadata
const mockData = [{ id: 'node-1' }, { id: 'node-2' }];
const totalRecords = 42;
return res.status(200).json({
success: true,
pagination: {
currentPage: page,
perPage: limit,
totalItems: totalRecords,
totalPages: Math.ceil(totalRecords / limit)
},
data: mockData
});
});
export default router;
An API without documentation is useless to frontend developers, mobile engineers, and third-party integrators. Without automation, handwritten documentation suffers from Documentation Drift: backend engineers update routes and schemas, while static Markdown docs remain outdated.
OpenAPI is the universal vendor-neutral standard for describing RESTful APIs. It defines endpoints, query parameters, request body schemas, authentication mechanisms, and expected HTTP status code responses in a machine-readable JSON or YAML document.
Swagger UI takes this OpenAPI specification document and automatically renders an interactive, searchable web portal with a runnable "Try It Out" sandbox that executes real HTTP requests against your live server directly from the browser!
import swaggerUi from 'swagger-ui-express';
import swaggerJsdoc from 'swagger-jsdoc';
// OpenAPI Specification Definition
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'DevAscent Telemetry Gateway API',
version: '1.0.0',
description: 'Production API documentation with live interactive testing'
},
servers: [{ url: 'http://localhost:5000/api/v1' }]
},
apis: ['./routes/*.js'] // Path to files containing @openapi comments
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
/**
* @openapi
* /nodes/{id}:
* get:
* summary: Retrieve single telemetry node by UUID
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Node retrieved successfully
* 404:
* description: Node not found
*/
Modern web applications rarely build everything from scratch; they integrate specialized third-party cloud APIs for maps (Mapbox, Google Maps), weather data (OpenWeatherMap), and notifications (Twilio, SendGrid).
Any API key embedded in React or client-side JavaScript (e.g., fetch(`https://api.weather.com?key=${REACT_APP_KEY}`)) is completely visible to the public via browser DevTools Network tabs and decompiled bundles. Malicious actors scrape these keys to consume thousands of dollars in billing or compromise infrastructure.
Always use the Backend Proxy Pattern:
/api/v1/weather?city=London.process.env.WEATHER_API_KEY, makes the call server-to-server, caches the result, and returns the sanitized payload to the frontend.// Simple in-memory cache to save external API costs and latency
const weatherCache = new Map();
app.get('/api/v1/weather', async (req, res, next) => {
try {
const city = (req.query.city || 'London').toLowerCase();
const cacheKey = `weather_${city}`;
// 1. Check cache (expires after 10 minutes)
const cached = weatherCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp < 10 * 60 * 1000)) {
return res.status(200).json({ source: 'cache', data: cached.payload });
}
// 2. Backend securely injects secret API key from environment
const apiKey = process.env.OPENWEATHER_API_KEY;
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`;
const apiResponse = await fetch(url);
if (!apiResponse.ok) {
return res.status(apiResponse.status).json({ error: 'Weather provider lookup failed.' });
}
const rawData = await apiResponse.json();
// 3. Sanitize and transform only the fields your UI requires
const sanitizedData = {
location: rawData.name,
temperatureC: rawData.main.temp,
conditions: rawData.weather[0].description,
humidity: rawData.main.humidity
};
// Store in cache
weatherCache.set(cacheKey, { timestamp: Date.now(), payload: sanitizedData });
return res.status(200).json({ source: 'live-upstream', data: sanitizedData });
} catch (err) {
next(err);
}
});
Handling financial transactions introduces the highest security stakes in web engineering. Under PCI-DSS compliance regulations, credit card numbers must never touch your application servers.
pm_104928).requires_payment_method.Payment results cannot rely solely on the browser redirectβthe customer might close their laptop, lose mobile signal, or manually close the tab after the card is charged!
Payment gateways solve this via Webhooks: the payment server calls an HTTP POST endpoint on your backend (e.g., /api/v1/stripe-webhook) to notify you that payment_intent.succeeded.
Cryptographic Signature Verification: To prevent hackers from spoofing fake "payment succeeded" notifications, Stripe signs the payload using an HMAC-SHA256 signature header (stripe-signature). Your backend mathematically verifies this signature using your shared Webhook Secret before provisioning any goods!
import express from 'express';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const webhookApp = express.Router();
// CRITICAL: Stripe signature verification REQUIRES the raw unparsed byte buffer!
// Do NOT run express.json() before this endpoint!
webhookApp.post(
'/webhook',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
// Cryptographically verify that Stripe sent this payload
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error(`β οΈ Webhook signature verification failed:`, err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle specific event type
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log(`π° Payment succeeded for amount: $${paymentIntent.amount / 100}`);
// Fulfill order in database: updateUserSubscription(paymentIntent.customer)
break;
case 'payment_intent.payment_failed':
console.warn('β Payment failed:', event.data.object.last_payment_error?.message);
break;
}
// Acknowledge receipt to Stripe to stop retries
res.status(200).json({ received: true });
}
);
This capstone builds a complete, production-grade API Service Gateway. It encapsulates rate-limiting, an internal REST resource, a secured third-party weather proxy with memory caching, and a test client using both Axios and Fetch:
import express from 'express';
const app = express();
app.use(express.json());
// Mock Database
const telemetryNodes = [
{ id: 'node-1', location: 'London', status: 'active' },
{ id: 'node-2', location: 'Tokyo', status: 'active' }
];
// 1. Internal REST Resource (GET all with pagination)
app.get('/api/v1/nodes', (req, res) => {
res.status(200).json({
success: true,
count: telemetryNodes.length,
data: telemetryNodes
});
});
// 2. Third-Party Proxy: Fetches external weather for a telemetry node
app.get('/api/v1/nodes/:id/weather', async (req, res) => {
const node = telemetryNodes.find(n => n.id === req.params.id);
if (!node) {
return res.status(404).json({ error: 'Telemetry node not found.' });
}
// Simulated safe external API call
const weatherMock = {
city: node.location,
tempC: 18.5,
humidity: 62,
source: 'open-meteo-proxy'
};
res.status(200).json({
nodeId: node.id,
weather: weatherMock
});
});
// Global 404 Route Fallback
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Cannot ${req.method} ${req.originalUrl}`
});
});
const PORT = 5050;
app.listen(PORT, () => console.log(`π Gateway listening on http://localhost:${PORT}`));
fetch() API NOT reject its Promise on HTTP 404 or 500 status codes?fetch() rejects strictly on physical network failures (DNS lookup failure, no internet connection, server connection timeout, or CORS blocks). Developers must manually inspect response.ok to catch HTTP status errors.
PUT is intended for full resource replacement: the client transmits the entire new representation of the entity, overwriting all existing fields. PUT is idempotent: executing the identical replacement 10 times produces the exact same server state. PATCH is intended for partial modifications: the client transmits only the specific fields to mutate (e.g., updating only an email address). PATCH is non-idempotent by default because partial instructions (such as increment operations) compound on repeated execution.
stripe-signature). The receiving server recalculates the HMAC using the raw unparsed body buffer; if the signatures don't match, the request is rejected as forged.
LIMIT x OFFSET y. It is easy to use but fails at high scale: running OFFSET 1000000 forces the database to read and discard 1 million rows from disk, causing high CPU latency. Additionally, if rows are inserted while a user is browsing, items shift and duplicate rows appear on subsequent pages. Cursor pagination uses a unique sequential pointer (e.g., WHERE id > $last_seen_id LIMIT 20), which jumps directly into a B-Tree index in O(1) time, ensuring zero duplicate records and stable performance regardless of page depth.
Idempotency-Key: e4b2...) during transactional POST requests. If a network timeout or dropped connection occurs after the server charges the customer's credit card but before the client receives the receipt, the client safely retries the request with the identical key. The server recognizes the key, recognizes it already charged the transaction, and returns the previous successful receipt without charging the card a second time!
swagger-jsdoc, documentation annotations live directly alongside route handlers in the codebase. Whenever developers update controllers or request validation schemas, the documentation updates synchronously, eliminating documentation drift.