DEV Community

vengadesa boopathi
vengadesa boopathi

Posted on

# APIs: The Invisible Infrastructure of Everything You Use

You've heard the word a thousand times. APIs. But most articles stop at the waiter analogy and call it a day. This one doesn't. By the end of this article you'll understand not just what an API is, but why it's designed the way it is, what actually happens at the network level when a request is made, how a server handles thousands of concurrent connections, and how to build and host your own API from scratch using Python and Flask. We'll also look at the mistakes developers make when building APIs and how to avoid them.

Let's go deep.


The Real Reason APIs Exist

Before HTTP, before REST, before JSON — software systems still needed to talk to each other. Early solutions were nightmares: shared memory regions, custom binary protocols, raw TCP sockets with hand-rolled parsing. Every integration was a one-off engineering project.

APIs are the answer to a design question: how do we let two systems communicate without either one needing to know the internal implementation details of the other?

This is the principle of encapsulation applied to networked systems. A good API is a contract. It says: "Send me a request in this shape, I'll send back a response in that shape. What I do in between is none of your business."

That contract is what makes software composable. Stripe doesn't care if you built your frontend in React or Django. They expose an API and you call it. The internals are invisible. This is not a convenience — it's a fundamental architectural principle that enables the entire modern software ecosystem.


What HTTP Actually Is (And Why APIs Use It)

Almost every API you'll encounter is an HTTP API. To understand APIs deeply, you need to understand what HTTP is doing under the hood.

HTTP (HyperText Transfer Protocol) is a text-based, application-layer protocol built on top of TCP/IP. When your browser (or your Python script) makes an HTTP request, here's what actually travels over the wire:

GET /v1/forecast?city=London HTTP/1.1
Host: api.weather.com
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Connection: keep-alive
Enter fullscreen mode Exit fullscreen mode

That's it. Plain text. The server reads this, parses it, does some work, and sends back:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 87
Date: Mon, 27 Mar 2026 12:00:00 GMT

{
  "city": "London",
  "temperature": 14,
  "unit": "C",
  "condition": "Overcast"
}
Enter fullscreen mode Exit fullscreen mode

Two blocks of text separated by a blank line. Everything above the blank line is headers (metadata about the request/response). Everything below is the body (the actual payload).

Understanding this is important because it demystifies the whole thing. There's no magic. An API endpoint is just a server that listens on a port, reads these text messages, and writes back other text messages. Every framework — Flask, FastAPI, Express, Rails — is ultimately doing this, just with a lot of ergonomic scaffolding on top.


The TCP Handshake: What Happens Before Your Request Even Sends

Before any HTTP data flows, TCP (Transmission Control Protocol) has to establish a connection. This is the three-way handshake:

  1. SYN — Your machine sends a SYN (synchronize) packet to the server's IP and port. "I want to connect."
  2. SYN-ACK — The server responds with SYN-ACK. "Okay, I acknowledge. I'm ready."
  3. ACK — Your machine sends ACK. "Great. Connection established."

Only after this handshake does your HTTP request actually get sent. This is why latency matters. If the server is geographically far from you, even before your API call is processed, you've already spent time just shaking hands.

With HTTP/1.1, connections can be kept alive (reused for multiple requests). With HTTP/2, multiple requests can be multiplexed over a single connection simultaneously. This is why upgrading your server infrastructure isn't just about CPU — network protocol choices have real performance implications.


DNS: How api.weather.com Becomes an IP Address

You don't make requests to IP addresses directly in most cases. You use domain names. But underneath, networks only understand numbers.

When you call https://api.weather.com/v1/forecast, before the TCP handshake even begins, your operating system does a DNS lookup:

  1. Checks the local cache. Found? Use it.
  2. Asks your router. Found? Use it.
  3. Asks a recursive DNS resolver (usually your ISP's or Google's 8.8.8.8).
  4. That resolver walks up the DNS hierarchy: root servers → .com TLD servers → weather.com authoritative nameserver.
  5. Gets back: api.weather.com → 104.22.6.49

Now your machine knows the actual IP to connect to. This entire process can happen in under 10 milliseconds when cached, or tens to hundreds of milliseconds on a cold lookup. This is why CDNs and DNS TTL tuning are real performance levers in production systems.


REST: A Style, Not a Standard

REST is not a protocol. It's not a library. It's not something you install. It's an architectural style — a set of constraints described by Roy Fielding in his 2000 PhD dissertation.

The six constraints of REST:

  1. Client-Server — Concerns are separated. The client handles the UI, the server handles data and logic. They communicate only through the interface.
  2. Stateless — Every request must contain all the information needed to process it. The server holds no session state between requests. This is why you send your auth token on every request instead of logging in once.
  3. Cacheable — Responses must declare whether they're cacheable. This enables CDNs and browser caches to reduce server load.
  4. Uniform Interface — This is the big one. All resources are identified by URLs. Actions are expressed via HTTP methods. Responses describe how to modify the resource (HATEOAS, though rarely implemented in practice).
  5. Layered System — The client doesn't know whether it's talking directly to the server or to a load balancer, a cache, or a gateway. Each layer only knows about the layer immediately above and below.
  6. Code on Demand (optional) — Servers can send executable code to clients (like JavaScript). Most REST APIs don't use this.

When people say "RESTful API," they usually mean: uses HTTP methods correctly, uses URLs as resource identifiers, returns JSON, and is stateless. The full Fielding definition is rarely implemented completely, especially HATEOAS.

The key practical takeaways:

  • Use nouns in URLs, not verbs: /users/123 not /getUser?id=123
  • Use HTTP methods for the action: GET /users/123, DELETE /users/123, PATCH /users/123
  • Use status codes correctly (more on this shortly)
  • Keep it stateless — don't store session data server-side between requests

HTTP Status Codes: Not Optional

Bad APIs return 200 OK with {"error": "user not found"} in the body. Don't be that API. Status codes exist precisely to communicate outcome at the protocol level, before the client even parses the body.

2xx — Success

Code Meaning Use When
200 OK GET, PUT, PATCH succeeded with a response body
201 Created POST successfully created a resource
204 No Content DELETE succeeded; nothing to return

3xx — Redirection

Code Meaning Use When
301 Moved Permanently Resource has a new URL forever
304 Not Modified Cached version is still valid

4xx — Client Error (The caller did something wrong)

Code Meaning Use When
400 Bad Request Malformed request, invalid parameters
401 Unauthorized No credentials / invalid credentials
403 Forbidden Valid credentials, but no permission
404 Not Found Resource doesn't exist
409 Conflict Duplicate entry, version conflict
422 Unprocessable Entity Validation failed (correct format, invalid values)
429 Too Many Requests Rate limit exceeded

5xx — Server Error (Something on your end broke)

Code Meaning Use When
500 Internal Server Error Unhandled exception, bug in your code
502 Bad Gateway Upstream server returned invalid response
503 Service Unavailable Server is overloaded or in maintenance
504 Gateway Timeout Upstream server timed out

The distinction between 401 and 403 confuses people: 401 means "I don't know who you are", 403 means "I know who you are, and you can't do this."


Flask: The Minimal Python Web Framework

Flask is a micro web framework for Python. "Micro" doesn't mean limited — it means Flask gives you the essentials and stays out of your way. It doesn't dictate how you structure your project, which database to use, or how to handle authentication. That's your call.

Flask's core job: map incoming HTTP requests to Python functions and turn their return values into HTTP responses.

Install Flask

pip install flask
Enter fullscreen mode Exit fullscreen mode

Your First API Endpoint

from flask import Flask, jsonify, request

app = Flask(__name__)

@app.route('/hello', methods=['GET'])
def hello():
    name = request.args.get('name', 'World')
    return jsonify({"message": f"Hello, {name}!"})

if __name__ == '__main__':
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

Run it:

python app.py
Enter fullscreen mode Exit fullscreen mode

Test it:

curl "http://127.0.0.1:5000/hello?name=Venki"
# {"message": "Hello, Venki!"}
Enter fullscreen mode Exit fullscreen mode

What just happened? @app.route('/hello', methods=['GET']) is a decorator. It registers the function hello as the handler for HTTP GET requests to the path /hello. When a request comes in, Flask's router matches the path, calls the function, and wraps the return value into an HTTP response.


Flask: The Full CRUD Pattern

Let's build a complete in-memory user management API. This is the foundation of every REST API you'll ever write.

from flask import Flask, jsonify, request, abort

app = Flask(__name__)

# In-memory "database" — a simple dict
users = {}
next_id = 1


# GET /users — list all users
@app.route('/users', methods=['GET'])
def list_users():
    return jsonify(list(users.values())), 200


# GET /users/<id> — get a single user
@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = users.get(user_id)
    if user is None:
        abort(404)
    return jsonify(user), 200


# POST /users — create a new user
@app.route('/users', methods=['POST'])
def create_user():
    global next_id
    data = request.get_json()

    if not data or 'name' not in data or 'email' not in data:
        return jsonify({"error": "name and email are required"}), 400

    user = {
        "id": next_id,
        "name": data['name'],
        "email": data['email']
    }
    users[next_id] = user
    next_id += 1

    return jsonify(user), 201


# PUT /users/<id> — replace a user completely
@app.route('/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    if user_id not in users:
        abort(404)

    data = request.get_json()
    if not data or 'name' not in data or 'email' not in data:
        return jsonify({"error": "name and email are required"}), 400

    users[user_id] = {"id": user_id, "name": data['name'], "email": data['email']}
    return jsonify(users[user_id]), 200


# PATCH /users/<id> — update specific fields
@app.route('/users/<int:user_id>', methods=['PATCH'])
def patch_user(user_id):
    if user_id not in users:
        abort(404)

    data = request.get_json()
    user = users[user_id]

    # Only update fields that were provided
    user['name'] = data.get('name', user['name'])
    user['email'] = data.get('email', user['email'])

    return jsonify(user), 200


# DELETE /users/<id> — remove a user
@app.route('/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    if user_id not in users:
        abort(404)

    del users[user_id]
    return '', 204


if __name__ == '__main__':
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

Test with curl:

# Create
curl -X POST http://127.0.0.1:5000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Venki", "email": "v@example.com"}'

# Read
curl http://127.0.0.1:5000/users/1

# Update
curl -X PATCH http://127.0.0.1:5000/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Venkatesh"}'

# Delete
curl -X DELETE http://127.0.0.1:5000/users/1
Enter fullscreen mode Exit fullscreen mode

Flask: Request Object Deep Dive

request is Flask's global object representing the current HTTP request. It's thread-safe via context locals — each request gets its own isolated instance.

from flask import request

@app.route('/demo', methods=['POST'])
def demo():
    # Query string: /demo?page=2&limit=10
    page = request.args.get('page', 1, type=int)
    limit = request.args.get('limit', 10, type=int)

    # JSON body
    body = request.get_json()           # Returns dict or None
    body = request.get_json(silent=True) # Returns None instead of raising on bad JSON

    # Form data (application/x-www-form-urlencoded)
    username = request.form.get('username')

    # Raw body bytes
    raw = request.data

    # Headers
    auth_header = request.headers.get('Authorization')
    content_type = request.content_type

    # HTTP method
    method = request.method  # 'GET', 'POST', etc.

    # Full URL info
    full_url = request.url           # http://127.0.0.1:5000/demo?page=2
    base_url = request.base_url      # http://127.0.0.1:5000/demo
    host = request.host              # 127.0.0.1:5000
    path = request.path              # /demo

    # Client IP
    client_ip = request.remote_addr

    return jsonify({"page": page, "method": method})
Enter fullscreen mode Exit fullscreen mode

Flask: Error Handling Done Right

Flask lets you register global error handlers. Use them to return consistent error responses instead of letting exceptions bubble up as ugly 500 HTML pages.

from flask import Flask, jsonify
from werkzeug.exceptions import HTTPException

app = Flask(__name__)


@app.errorhandler(400)
def bad_request(e):
    return jsonify({"error": "Bad Request", "message": str(e.description)}), 400


@app.errorhandler(404)
def not_found(e):
    return jsonify({"error": "Not Found", "message": str(e.description)}), 404


@app.errorhandler(405)
def method_not_allowed(e):
    return jsonify({"error": "Method Not Allowed"}), 405


@app.errorhandler(500)
def internal_error(e):
    return jsonify({"error": "Internal Server Error"}), 500


# Catch-all for any other HTTP exception
@app.errorhandler(HTTPException)
def handle_http_exception(e):
    return jsonify({
        "error": e.name,
        "message": e.description,
        "status_code": e.code
    }), e.code
Enter fullscreen mode Exit fullscreen mode

Now your API always returns JSON errors, never HTML. Your clients will thank you.


Flask Blueprints: Structuring a Real Application

When your API grows beyond a handful of routes, a single app.py becomes unmanageable. Flask's Blueprint system lets you split routes into separate modules.

project/
├── app.py
├── routes/
│   ├── __init__.py
│   ├── users.py
│   └── products.py
Enter fullscreen mode Exit fullscreen mode

routes/users.py:

from flask import Blueprint, jsonify

users_bp = Blueprint('users', __name__, url_prefix='/users')

@users_bp.route('/', methods=['GET'])
def list_users():
    return jsonify({"users": []})

@users_bp.route('/<int:user_id>', methods=['GET'])
def get_user(user_id):
    return jsonify({"id": user_id})
Enter fullscreen mode Exit fullscreen mode

app.py:

from flask import Flask
from routes.users import users_bp
from routes.products import products_bp

app = Flask(__name__)
app.register_blueprint(users_bp)
app.register_blueprint(products_bp)

if __name__ == '__main__':
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

Blueprints let you keep each resource's routes, logic, and error handling together. This is how you scale a Flask project without descending into chaos.


Middleware: Code That Runs Around Every Request

Sometimes you need logic that applies to every request — logging, authentication checks, adding response headers. Flask handles this with before_request and after_request hooks.

import time
import uuid
from flask import Flask, jsonify, request, g

app = Flask(__name__)


@app.before_request
def start_timer():
    # g is Flask's per-request global storage
    g.start_time = time.time()
    g.request_id = str(uuid.uuid4())


@app.after_request
def log_request(response):
    duration_ms = (time.time() - g.start_time) * 1000
    print(
        f"[{g.request_id}] {request.method} {request.path} "
        f"{response.status_code} ({duration_ms:.1f}ms)"
    )
    # Add standard headers to every response
    response.headers['X-Request-ID'] = g.request_id
    response.headers['X-Response-Time'] = f"{duration_ms:.1f}ms"
    return response
Enter fullscreen mode Exit fullscreen mode

This pattern is how real APIs implement request tracing, logging, CORS headers, and rate limiting in a centralized way.


Authentication: Protecting Your Endpoints

Any API exposed to the internet needs authentication. Here's a simple API key implementation:

from flask import Flask, jsonify, request
from functools import wraps

app = Flask(__name__)

# In production, store these in a database, not a dict
VALID_API_KEYS = {
    "key_abc123": "user_1",
    "key_xyz789": "user_2",
}


def require_api_key(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        api_key = request.headers.get('X-API-Key')
        if not api_key or api_key not in VALID_API_KEYS:
            return jsonify({"error": "Unauthorized"}), 401
        # Attach user info to the request context
        request.current_user = VALID_API_KEYS[api_key]
        return f(*args, **kwargs)
    return decorated


@app.route('/protected', methods=['GET'])
@require_api_key
def protected_route():
    return jsonify({
        "message": f"Hello, {request.current_user}!",
        "data": "This is private data."
    })
Enter fullscreen mode Exit fullscreen mode

Test it:

# No key — 401
curl http://127.0.0.1:5000/protected

# Wrong key — 401
curl -H "X-API-Key: wrong" http://127.0.0.1:5000/protected

# Valid key — 200
curl -H "X-API-Key: key_abc123" http://127.0.0.1:5000/protected
Enter fullscreen mode Exit fullscreen mode

Rate Limiting: Protecting Your API From Abuse

Without rate limiting, a single misbehaving client can overwhelm your server. The flask-limiter library handles this cleanly.

pip install flask-limiter
Enter fullscreen mode Exit fullscreen mode
from flask import Flask, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

app = Flask(__name__)

limiter = Limiter(
    app=app,
    key_func=get_remote_address,  # Rate limit per client IP
    default_limits=["100 per hour", "10 per minute"]
)


@app.route('/public', methods=['GET'])
def public_route():
    return jsonify({"data": "Anyone can call this, up to the default limits"})


@app.route('/expensive', methods=['GET'])
@limiter.limit("5 per minute")  # Stricter limit for resource-heavy endpoints
def expensive_route():
    # Imagine this calls an LLM or does heavy computation
    return jsonify({"result": "expensive computation"})
Enter fullscreen mode Exit fullscreen mode

When a client exceeds the limit, they receive a 429 status code automatically.


Consuming APIs with Python's requests Library

Building APIs is only half the picture. Here's how to consume them properly.

Basic Usage

import requests

response = requests.get(
    'https://api.github.com/users/torvalds',
    headers={'Accept': 'application/vnd.github.v3+json'}
)

print(response.status_code)   # 200
print(response.headers)       # Response headers dict
data = response.json()        # Parsed JSON body
print(data['public_repos'])   # Number of public repos
Enter fullscreen mode Exit fullscreen mode

Handling Errors Properly

import requests
from requests.exceptions import RequestException, Timeout, ConnectionError

def fetch_user(user_id: int) -> dict | None:
    try:
        response = requests.get(
            f'https://api.example.com/users/{user_id}',
            headers={'Authorization': 'Bearer your_token'},
            timeout=5  # Always set a timeout
        )
        response.raise_for_status()  # Raises HTTPError for 4xx and 5xx
        return response.json()

    except Timeout:
        print("Request timed out")
        return None
    except ConnectionError:
        print("Could not connect to the server")
        return None
    except requests.HTTPError as e:
        print(f"HTTP error: {e.response.status_code}{e.response.text}")
        return None
    except RequestException as e:
        print(f"Request failed: {e}")
        return None
Enter fullscreen mode Exit fullscreen mode

Using a Session for Repeated Calls

Every requests.get() call opens and closes a new TCP connection. If you're making many calls to the same API, use a Session to reuse the connection:

import requests

session = requests.Session()
session.headers.update({
    'Authorization': 'Bearer your_token',
    'Accept': 'application/json'
})

# These reuse the same underlying TCP connection (keep-alive)
r1 = session.get('https://api.example.com/users/1')
r2 = session.get('https://api.example.com/users/2')
r3 = session.get('https://api.example.com/products/5')

session.close()
Enter fullscreen mode Exit fullscreen mode

This can reduce latency significantly when making dozens of API calls.


Hosting Your API Locally — And What Actually Happens

Flask's built-in dev server (app.run()) is not production-grade. It handles one request at a time. It's purely for development. Here's what it does internally when you call it:

  1. Creates a TCP socketsocket.socket(socket.AF_INET, socket.SOCK_STREAM)
  2. Binds to addresssocket.bind(('0.0.0.0', 5000))
    • 0.0.0.0 means "all network interfaces" — your localhost AND your LAN IP
    • 127.0.0.1 would mean "localhost only"
  3. Starts listeningsocket.listen(n) — OS queues incoming connections
  4. Accept loop — for each connection: conn, addr = socket.accept()
  5. Read the HTTP request from the socket
  6. Parse it — method, path, headers, body
  7. Route it — Flask matches path to your function
  8. Run your function — generates the response
  9. Write the response back to the socket
  10. Handle keep-alive or close the connection

Flask's dev server does all this synchronously, single-threaded. For production, you replace it with Gunicorn (for synchronous apps) or Uvicorn (for async apps), which run multiple workers.

Running on Your Local Network

app.run(host='0.0.0.0', port=5000, debug=True)
Enter fullscreen mode Exit fullscreen mode

Find your machine's local IP:

# Linux / macOS
ip addr show | grep 'inet ' | grep -v 127.0.0.1

# Windows
ipconfig | findstr IPv4
Enter fullscreen mode Exit fullscreen mode

Let's say it's 192.168.1.100. Any device on your Wi-Fi can now hit:

http://192.168.1.100:5000/users
Enter fullscreen mode Exit fullscreen mode

Exposing to the Internet with ngrok

pip install ngrok
ngrok http 5000
Enter fullscreen mode Exit fullscreen mode

ngrok tunnels traffic from a public URL (https://abc123.ngrok.io) to your local port 5000. Useful for demos, webhooks, and testing.


The Gap Between Development and Production

Concern Dev (Flask dev server) Production
Concurrency Single-threaded Gunicorn/Uvicorn workers
HTTPS No Nginx/Caddy as reverse proxy
Process management Manual systemd / Docker / Kubernetes
Environment variables Hardcoded .env + secrets manager
Logging print() Structured logging (JSON) to ELK/CloudWatch
Error tracking Terminal output Sentry
Database In-memory dict PostgreSQL / MySQL with connection pooling
Static files Flask serves them CDN / Nginx

A minimal production Flask deployment:

[Client] → [Nginx (HTTPS termination)] → [Gunicorn (WSGI server)] → [Flask App]
                                                                   → [PostgreSQL]
Enter fullscreen mode Exit fullscreen mode
# Install gunicorn
pip install gunicorn

# Run 4 worker processes
gunicorn -w 4 -b 0.0.0.0:8000 app:app
Enter fullscreen mode Exit fullscreen mode

Common API Design Mistakes (And How to Avoid Them)

1. Using verbs in URLs

# Wrong
GET /getUser?id=5
POST /createUser
DELETE /deleteUser?id=5

# Right
GET /users/5
POST /users
DELETE /users/5
Enter fullscreen mode Exit fullscreen mode

2. Not versioning your API

If you ever need to make a breaking change, you'll thank yourself for having /v1/ in your URLs from day one. Clients can stay on v1 while you build v2.

GET /v1/users/5
GET /v2/users/5
Enter fullscreen mode Exit fullscreen mode

3. Ignoring status codes

Don't return 200 OK with an error message in the body. Use the appropriate 4xx or 5xx code.

4. No pagination on list endpoints

# Bad — returns everything
@app.route('/users', methods=['GET'])
def list_users():
    return jsonify(all_10_million_users)

# Good — paginated
@app.route('/users', methods=['GET'])
def list_users():
    page = request.args.get('page', 1, type=int)
    per_page = min(request.args.get('per_page', 20, type=int), 100)
    offset = (page - 1) * per_page
    paginated = list(users.values())[offset:offset + per_page]
    return jsonify({
        "data": paginated,
        "page": page,
        "per_page": per_page,
        "total": len(users)
    })
Enter fullscreen mode Exit fullscreen mode

5. Not validating input

Never trust client input. Always validate before processing:

from flask import request, jsonify

@app.route('/users', methods=['POST'])
def create_user():
    data = request.get_json(silent=True)

    if data is None:
        return jsonify({"error": "Request body must be valid JSON"}), 400

    errors = {}
    if not data.get('name') or not isinstance(data['name'], str):
        errors['name'] = "Required, must be a string"
    if not data.get('email') or '@' not in str(data.get('email', '')):
        errors['email'] = "Required, must be a valid email"

    if errors:
        return jsonify({"error": "Validation failed", "details": errors}), 422

    # Safe to proceed
    ...
Enter fullscreen mode Exit fullscreen mode

6. Exposing internal details in error messages

# Bad — exposes your database schema and internal paths
{"error": "psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint \"users_email_key\" DETAIL: Key (email)=(v@example.com) already exists."}

# Good
{"error": "Conflict", "message": "An account with this email already exists"}
Enter fullscreen mode Exit fullscreen mode

A Complete, Production-Ready Flask API Template

Here's a starting point that incorporates everything above:

import time
import uuid
import logging
from flask import Flask, jsonify, request, g
from werkzeug.exceptions import HTTPException

# Configure structured logging
logging.basicConfig(
    level=logging.INFO,
    format='{"time": "%(asctime)s", "level": "%(levelname)s", "message": "%(message)s"}'
)
logger = logging.getLogger(__name__)

app = Flask(__name__)


# ─── Request lifecycle hooks ───────────────────────────────────────────────────

@app.before_request
def before():
    g.start_time = time.time()
    g.request_id = str(uuid.uuid4())


@app.after_request
def after(response):
    duration_ms = round((time.time() - g.start_time) * 1000, 2)
    logger.info(
        f"method={request.method} path={request.path} "
        f"status={response.status_code} duration_ms={duration_ms} "
        f"request_id={g.request_id}"
    )
    response.headers['X-Request-ID'] = g.request_id
    return response


# ─── Global error handlers ─────────────────────────────────────────────────────

@app.errorhandler(HTTPException)
def handle_http_exception(e):
    return jsonify({
        "error": e.name,
        "message": e.description,
        "request_id": g.get('request_id')
    }), e.code


@app.errorhandler(Exception)
def handle_unexpected_error(e):
    logger.exception("Unhandled exception")
    return jsonify({
        "error": "Internal Server Error",
        "request_id": g.get('request_id')
    }), 500


# ─── Routes ────────────────────────────────────────────────────────────────────

@app.route('/health', methods=['GET'])
def health():
    return jsonify({"status": "ok", "uptime_ms": round(time.time() * 1000)}), 200


# Register blueprints here
# app.register_blueprint(users_bp)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)
Enter fullscreen mode Exit fullscreen mode

Where to Go From Here

You now understand APIs at the level most developers never reach. The next steps:

Deepen your Flask knowledge: Add SQLAlchemy for a real database, Flask-Migrate for schema migrations, and marshmallow or Pydantic for request/response serialization.

Learn FastAPI: If you want async support, automatic OpenAPI docs, and Pydantic validation built in, FastAPI is the modern Python alternative. It builds on the same ASGI/WSGI concepts.

Understand authentication properly: Study OAuth 2.0 and JWT (JSON Web Tokens). These are how real-world authentication flows work.

Build something: The only way to really learn API design is to build one, use it, find its rough edges, and fix them. Start with a small project you actually care about.

Read the HTTP spec: RFC 7230–7235. Not the whole thing, but skim it. Knowing what the protocol actually specifies versus what frameworks paper over gives you a durable mental model.


If you found this useful, follow for more deep-dives into Python, backend systems, and software engineering fundamentals.

Top comments (0)