Building REST APIs with Python: A Practical Guide
The design decisions behind a REST API that other programs can depend on: resource URLs, what GET, POST, PUT and DELETE promise, the status codes that matter, validation at the boundary, one error format, and pagination. Shown with a small Flask API exercised through its test client.
GloryTecks Team
Training & Placement Faculty
Last updated: 24 September 2026
An API is a promise to people you may never meet: the front-end developer, the mobile app, the partner's integration script. They will write code against every URL, status code and error message you return, and they will break when you change them. So the design decisions matter more than the framework. This guide covers the decisions, framework-independent, then shows them in a small Flask API you can run.
Resources and URLs
REST organises an API around resources, the nouns of your domain, each with a URL. Operations are expressed with HTTP methods on those URLs, not with verbs in the path:
| Instead of | Use |
|---|---|
POST /createTask | POST /api/v1/tasks |
GET /getTask?id=42 | GET /api/v1/tasks/42 |
POST /tasks/42/delete | DELETE /api/v1/tasks/42 |
GET /tasksForUser?user=7 | GET /api/v1/users/7/tasks |
Use plural nouns for collections, keep nesting to one level (/users/7/tasks, not /users/7/projects/3/tasks/42), and put a version in the path (/v1) from the start. It is the cheapest way to change things later without breaking existing clients.
What each method promises
HTTP methods carry guarantees that clients, proxies and retry logic rely on (RFC 9110: HTTP semantics). Safe methods do not change anything on the server. Idempotent methods can be repeated with the same effect as doing them once, so a client can retry them after a timeout:
| Method | Use it to | Safe | Idempotent |
|---|---|---|---|
| GET | Read a resource or collection | Yes | Yes |
| POST | Create a resource, or trigger an action | No | No |
| PUT | Replace a resource with the body you send | No | Yes |
| PATCH | Change some fields of a resource | No | Not necessarily |
| DELETE | Remove a resource | No | Yes |
PATCH is defined in a separate specification, RFC 5789, and is not idempotent by definition. The practical consequence: a GET endpoint must never modify data, and a POST that times out may or may not have created the resource. APIs that take payments or orders often accept an idempotency key header on POST, so that a retried request is recognised and not processed twice.
Status codes worth getting right
A client's code branches on the status code before it reads the body. You do not need dozens; you need these used consistently:
| Code | Meaning | Typical case |
|---|---|---|
| 200 OK | Success, with a body | GET, or an update that returns the resource |
| 201 Created | A resource was created | POST, with a Location header pointing at it |
| 204 No Content | Success, nothing to return | DELETE |
| 400 Bad Request | The request is malformed | Unparseable JSON |
| 401 Unauthorized | Not authenticated | Missing or invalid token |
| 403 Forbidden | Authenticated, not allowed | A user editing someone else's task |
| 404 Not Found | No such resource | Unknown ID |
| 409 Conflict | Conflicts with current state | Creating a duplicate username |
| 422 Unprocessable Content | Well-formed but invalid | A title that is empty |
| 500 Internal Server Error | Your bug | Never return it on purpose |
Note the 401 and 403 distinction, which is often blurred: 401 means "who are you?", 403 means "I know who you are, and no".
One error format everywhere
Clients should be able to handle every error with one piece of code. RFC 9457 defines a standard shape for that, served with the media type application/problem+json: a title, the status, a human-readable detail, and optional fields of your own (RFC 9457: problem details). Adopting it means you never debate the error format again.
A small Flask implementation
The API below manages tasks, keeping data in a dictionary so the example stays self-contained. It applies each rule above: versioned resource URLs, validation before anything is stored, 201 with a Location header, 204 on delete, problem-details errors, and limit/offset pagination (Flask quickstart).
from flask import Flask, jsonify, request
app = Flask(__name__)
TASKS: dict[int, dict] = {}
NEXT_ID = 1
def problem(status: int, title: str, detail: str):
body = {"type": "about:blank", "title": title, "status": status, "detail": detail}
return jsonify(body), status, {"Content-Type": "application/problem+json"}
@app.get("/api/v1/tasks")
def list_tasks():
limit = min(request.args.get("limit", default=20, type=int), 100)
offset = request.args.get("offset", default=0, type=int)
items = list(TASKS.values())
return {"items": items[offset:offset + limit], "total": len(items), "limit": limit, "offset": offset}
@app.post("/api/v1/tasks")
def create_task():
global NEXT_ID
data = request.get_json(silent=True)
if not isinstance(data, dict):
return problem(400, "Bad Request", "Body must be a JSON object.")
title = str(data.get("title", "")).strip()
if not title or len(title) > 200:
return problem(422, "Invalid task", "title is required and must be 1-200 characters.")
task = {"id": NEXT_ID, "title": title, "done": False}
TASKS[NEXT_ID] = task
NEXT_ID += 1
return task, 201, {"Location": f"/api/v1/tasks/{task['id']}"}
@app.get("/api/v1/tasks/<int:task_id>")
def get_task(task_id: int):
if task_id not in TASKS:
return problem(404, "Not Found", f"No task with id {task_id}.")
return TASKS[task_id]
@app.delete("/api/v1/tasks/<int:task_id>")
def delete_task(task_id: int):
if TASKS.pop(task_id, None) is None:
return problem(404, "Not Found", f"No task with id {task_id}.")
return "", 204Flask's test client sends requests to the app without starting a server, which is also how you would test it (Flask: testing):
client = app.test_client()
r = client.post("/api/v1/tasks", json={"title": "Write API docs"})
print(r.status_code, r.headers["Location"], r.get_json())
r = client.post("/api/v1/tasks", json={"title": " "})
print(r.status_code, r.content_type, r.get_json()["detail"])
client.post("/api/v1/tasks", json={"title": "Add pagination"})
r = client.get("/api/v1/tasks?limit=1&offset=1")
print(r.status_code, r.get_json())
print(client.delete("/api/v1/tasks/1").status_code, client.get("/api/v1/tasks/1").status_code)201 /api/v1/tasks/1 {'done': False, 'id': 1, 'title': 'Write API docs'}
422 application/problem+json title is required and must be 1-200 characters.
200 {'items': [{'done': False, 'id': 2, 'title': 'Add pagination'}], 'limit': 1, 'offset': 1, 'total': 2}
204 404Decisions to make before you ship
- Validation at the boundary. Check every field before anything touches storage, and report all problems in the 422 body. Libraries such as Pydantic turn a class definition into that validation; see Pydantic explained.
- Pagination on every list. Return a page, never the whole table. Limit/offset is simple. Cursor pagination (return a
nexttoken) stays correct when rows are inserted while a client is paging, at the cost of not jumping to page 50. - A real database. Swap the dictionary for a database layer with transactions; the SQLAlchemy tutorial covers one.
- Authentication over HTTPS only, usually with bearer tokens, and authorisation checks on every object a request touches, not just on login.
- Documentation clients can test against. An OpenAPI description, whether written by hand or generated, is the contract. Frameworks such as FastAPI generate it from your code; the FastAPI guide builds the same kind of API that way.
Changing an API without breaking clients
Once clients depend on an API, some changes are safe and some are not. Adding a new endpoint, a new optional request field or a new field in a response is usually safe, because well-written clients ignore fields they do not know. Removing or renaming a field, changing its type, making an optional field required, or changing what a status code means will break someone. Make breaking changes in a new version (/api/v2/...), keep the old version running for an announced period, and tell clients when it will be switched off. Documenting which changes you consider non-breaking lets client developers write code that tolerates them.
Choosing a framework
The rules above hold in any framework. Flask keeps things minimal, as shown. FastAPI adds validation and API docs from type hints. Django REST framework sits on Django's ORM, admin and authentication. Flask vs FastAPI vs Django compares them for different kinds of projects.
REST API development with Flask is part of the web development module in the Python Programming course.
Sources
- RFC 9110: HTTP Semantics — RFC Editor
- RFC 9457: Problem Details for HTTP APIs — RFC Editor
- Flask Quickstart — Flask documentation
- Testing Flask Applications — Flask documentation
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Python?
Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.
Related articles
Learn Python from Scratch: A Complete 2026 Roadmap
How to learn Python from zero: install a current version (including the new Windows install manager), then work through seven stages, from values and loops to files, virtual environments, classes and tests, each ending with a small program you should be able to write on your own.
Python Interview Questions: Top 50 with Answers
Fifty conceptual Python interview questions with short, precise answers, grouped by topic: basics, types and mutability, data structures, functions and scope, OOP, iterators and decorators, errors and files, and internals, with runnable examples for the ones people most often get wrong.
Python for Data Science: The Essential Libraries
The Python data science stack explained by the job each library does: NumPy for arrays, pandas for tables, Matplotlib and seaborn for charts, SciPy and statsmodels for statistics, scikit-learn for models and Jupyter for exploration, with one runnable example that passes a real dataset through them.