Serving Your First Endpoint

A web framework is a wrapper over one function from request to response; here is that function, returning JSON.

Syntaxdef app(environ, start_response) -> Iterable[bytes] # the WSGI contract

Under Flask, Django and FastAPI there is a single function. It takes a request and returns a response, and everything a framework adds — routing, parsing, validation, sessions — is a way of writing that function more conveniently.

The function underneath

For synchronous frameworks the contract is WSGI: a callable taking (environ, start_response). environ is a dict of CGI-style keys (REQUEST_METHOD, PATH_INFO, QUERY_STRING, wsgi.input for the body). You call start_response(status, headers) with the status as a string — "200 OK", not 200 — and headers as a list of pairs, then return an iterable of bytes objects. Returning a str is a common first mistake. ASGI is the asynchronous equivalent, async def app(scope, receive, send), and is what FastAPI implements.

What the framework does for you

It parses the body, matches the route, coerces types, serialises your return value and sets the headers. In FastAPI, @app.get("/health") is shorthand for app.add_api_route("/health", health, methods=["GET"]); in Flask, @app.route("/health") is documented as a shortcut for app.add_url_rule("/health", "health", health). Same registration, two spellings, and you will meet the long form in code that builds routes in a loop.

Getting JSON right

Three things have to agree: the body is JSON bytes, Content-Type is application/json, and the status matches what happened — 201 with a Location header for a create, 204 and no body for a delete. Without the header a client reads your JSON as plain text.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Adding a /health endpoint a load balancer can poll.
  • Exposing an internal script as an HTTP service without learning a framework first.
  • Understanding what a WSGI or ASGI error in a traceback is actually telling you.
  • Writing middleware, which is a function that wraps the one in this lesson.

More examples

The same endpoint in Flask, both spellings

The decorator and add_url_rule are the same call, and the second form is what you need the moment routes are generated. The endpoint name (the second argument) is what url_for refers to, and it defaults to the function's __name__.

Example · python
from flask import Flask, jsonify

app = Flask(__name__)

# Shorthand.
@app.route("/health", methods=["GET"])
def health():
    return jsonify(status="ok"), 200     # jsonify sets Content-Type for you

# Identical registration, written out. route() is documented as a shortcut for
# add_url_rule() with the view_func argument -- useful when the routes come
# from a list or a plugin rather than from decorators.
def version():
    return jsonify(version="1.4.0"), 200

app.add_url_rule("/version", "version", version, methods=["GET"])

# Development only: flask run, or app.run(). In production, a WSGI server:
#   gunicorn --workers 4 'myapp:app'

FastAPI, which is ASGI

Note that `async def` here is a choice, not a requirement — FastAPI runs a plain `def` endpoint in a thread pool. Pick async only once the handler actually awaits something, which the async stage of this course goes into.

Example · python
from fastapi import FastAPI, status

app = FastAPI()

# Shorthand: a dict return is serialised to JSON with Content-Type set and 200.
@app.get("/health")
async def health():
    return {"status": "ok"}

# Explicit about the status, as a create should be.
@app.post("/orders", status_code=status.HTTP_201_CREATED)
async def create_order(payload: dict):
    return {"id": 1041, **payload}

# Longhand, for routes registered dynamically:
#   app.add_api_route("/health", health, methods=["GET"])
#
# Run it: uvicorn main:app --port 8000   (main.py holding `app`)

Serving the raw WSGI app over a port

Worth running once to see that nothing magic happens between the socket and your function. Bind to 127.0.0.1 rather than the empty string so a development server is not reachable from the rest of the network.

Example · python
from wsgiref.simple_server import make_server

def app(environ, start_response):
    body = b'{"status": "ok"}'
    start_response("200 OK", [("Content-Type", "application/json"),
                              ("Content-Length", str(len(body)))])
    return [body]

# make_server(host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler)
with make_server("127.0.0.1", 8000, app) as httpd:
    print("serving on http://127.0.0.1:8000")
    httpd.handle_request()     # one request, then exit; serve_forever() to keep going

Discussion

  • Be the first to comment on this lesson.