Building a REST API
Reviewed & published by Brayan K
A REST API is a web interface that lets programs talk to each other over HTTP using standard verbs — GET, POST, PUT, and DELETE — to read and change structured data instead of returning HTML pages.
Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll build a small JSON API in Flask, map HTTP methods to create-read-update-delete operations, and return proper status codes so any client can use your service.
What You'll Learn in This Lesson
- What REST means and how it uses HTTP verbs
- How verbs map to create, read, update, delete
- How to return data with jsonify()
- How to read incoming data with request.get_json()
- How to set status codes like 201 and 404
- How to design clean resource URLs
1 REST and HTTP Verbs
REST (Representational State Transfer) is a set of conventions for building APIs around resources — things like users, articles, or tasks. Each resource has a URL, and you act on it using HTTP verbs.
These four verbs map cleanly onto the four basic data operations, often called CRUD: Create, Read, Update, Delete. A well-designed API is predictable because every client already knows what each verb means.
🧩 Verbs map to CRUD:
- GET /tasks — read all tasks (Read)
- POST /tasks — create a new task (Create)
- PUT /tasks/1 — update task 1 (Update)
- DELETE /tasks/1 — remove task 1 (Delete)
2 Reading Data with GET and jsonify()
The jsonify() helper turns a Python dictionary or list into a proper JSON response with the correct Content-Type header. Pair it with a GET route to expose your data.
The API below lists all tasks at /tasks and a single task at /tasks/<id>, returning 404 when the id doesn't exist. Run it with flask run and visit the URLs to see the JSON.
# app.py — a read-only tasks API
from flask import Flask, jsonify, abort
app = Flask(__name__)
tasks = [
{"id": 1, "title": "Learn Flask", "done": False},
{"id": 2, "title": "Build an API", "done": False},
]
@app.route("/tasks", methods=["GET"])
def list_tasks():
return jsonify(tasks) # 200 OK with a JSON array
@app.route("/tasks/<int:task_id>", methods=["GET"])
def get_task(task_id):
for task in tasks:
if task["id"] == task_id:
return jsonify(task)
abort(404) # no task with that id
# Simulate the lookup without a running server.
def find_task(task_id):
for task in tasks:
if task["id"] == task_id:
return task, 200
return {"error": "Not found"}, 404
print(find_task(1))
print(find_task(99))
# Run live with flask run, then visit /tasks and /tasks/1
# ✅ Expected output:
# ({'id': 1, 'title': 'Learn Flask', 'done': False}, 200)
# ({'error': 'Not found'}, 404)jsonify() serializes lists and dictionaries automatically. You return it just like any other response — and you can attach a status code by returning a tuple.
3 Creating, Updating & Deleting
To accept data from a client, read the request body with request.get_json(). Then return the right status code: 201 Created for a successful POST, 200 OK for an update, and 204 No Content for a delete.
The example below adds a task with POST, updates one with PUT, and removes one with DELETE — the full lifecycle of a resource.
# app.py — create, update, and delete tasks
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
tasks = [{"id": 1, "title": "Learn Flask", "done": False}]
@app.route("/tasks", methods=["POST"])
def create_task():
data = request.get_json() # parse the JSON body
if not data or "title" not in data:
abort(400) # bad request
new_task = {"id": len(tasks) + 1, "title": data["title"], "done": False}
tasks.append(new_task)
return jsonify(new_task), 201 # 201 Created
@app.route("/tasks/<int:task_id>", methods=["PUT"])
def update_task(task_id):
data = request.get_json()
for task in tasks:
if task["id"] == task_id:
task["done"] = data.get("done", task["done"])
return jsonify(task), 200 # 200 OK
abort(404)
@app.route("/tasks/<int:task_id>", methods=["DELETE"])
def delete_task(task_id):
global tasks
tasks = [t for t in tasks if t["id"] != task_id]
return "", 204 # 204 No Content
# Simulate a POST without a running server.
def create(data):
if "title" not in data:
return {"error": "title required"}, 400
task = {"id": 2, "title": data["title"], "done": False}
return task, 201
print(create({"title": "Write tests"}))
print(create({}))
# Run live with flask run, then POST to /tasks
# ✅ Expected output:
# ({'id': 2, 'title': 'Write tests', 'done': False}, 201)
# ({'error': 'title required'}, 400)🎯 Your Turn: Fill in the Blanks
Complete the API so it lists books with GET and adds one with POST returning 201. Replace each ___.
# 🎯 YOUR TURN: finish the books API
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
books = [{"id": 1, "title": "Flask 101"}]
@app.route("/books", methods=["___"]) # verb to read data
def list_books():
return ___(books) # return JSON
@app.route("/books", methods=["POST"])
def add_book():
data = request.___() # parse the JSON body
if not data or "title" not in data:
abort(400)
book = {"id": len(books) + 1, "title": data["title"]}
books.append(book)
return jsonify(book), ___ # status code for "created"
print("Routes:", [str(r) for r in app.url_map.iter_rules()])Common Errors & Quick Reference
❌ 405 Method Not Allowed
You sent a verb the route doesn't accept. List every method a route should handle in its methods=[...] argument, e.g. methods=["GET", "POST"].
The client didn't send a JSON body or the Content-Type header isn't application/json. Always guard with a check before using the data, and return 400 if it's missing.
| Task | Code |
|---|---|
| Return JSON | return jsonify(data) |
| Read request body | request.get_json() |
| Created status | return jsonify(item), 201 |
| Accept POST | methods=["POST"] |
🏆 Mini Challenge: A Notes API
Build a tiny notes API that supports reading all notes and deleting one by id.
- Add a GET route at /notes that returns all notes as JSON
- Add a DELETE route at /notes/<id> that removes a note
- Return 204 after a successful delete
# 🏆 Mini Challenge: a notes API
from flask import Flask, jsonify, abort
app = Flask(__name__)
notes = [{"id": 1, "text": "Buy milk"}, {"id": 2, "text": "Call Sam"}]
@app.route("/notes", methods=["GET"])
def list_notes():
return jsonify(notes), 200
@app.route("/notes/<int:note_id>", methods=["DELETE"])
def delete_note(note_id):
global notes
if not any(n["id"] == note_id for n in notes):
abort(404)
notes = [n for n in notes if n["id"] != note_id]
return "", 204
# Simulate a delete without a running server.
def delete(note_id):
if not any(n["id"] == note_id for n in notes):
return {"error": "Not found"}, 404
return "", 204
print(delete(1))
print(delete(99))
# Run live with flask run, then DELETE /notes/1❓ Frequently Asked Questions
Lesson complete — you built a working REST API!
You can map HTTP verbs to CRUD operations, return JSON with jsonify(), read bodies with request.get_json(), and send correct status codes like 201 and 404.
Practice quiz
Which HTTP verb is used to remove a resource?
- GET
- POST
- PUT
- DELETE
Answer: DELETE. DELETE removes a resource, mapping to the D in CRUD.
Which verb reads a resource without changing it?
- GET
- POST
- PUT
- PATCH
Answer: GET. GET reads data and should not modify the resource.
Which verb creates a new resource?
- GET
- POST
- DELETE
- HEAD
Answer: POST. POST creates a new resource, mapping to the C in CRUD.
Which verb typically updates an existing resource?
- GET
- POST
- PUT
- OPTIONS
Answer: PUT. PUT (or PATCH) updates an existing resource.
What status code signals a resource was successfully created?
- 200 OK
- 404 Not Found
- 500 Internal Error
- 201 Created
Answer: 201 Created. 201 Created is returned after a successful POST that creates a resource.
Which helper turns a Python dict or list into a JSON response?
- jsonify()
- render_template()
- str()
- marshal()
Answer: jsonify(). jsonify() serializes data and sets the application/json Content-Type.
How do you read a JSON request body in a Flask view?
- request.form
- request.get_json()
- request.args
- request.body
Answer: request.get_json(). request.get_json() parses the JSON body into a Python object.
What status fits a successful DELETE that returns no body?
- 201 Created
- 400 Bad Request
- 204 No Content
- 302 Found
Answer: 204 No Content. 204 No Content signals success with an empty response body.
What status should you return when a requested resource id doesn't exist?
- 200 OK
- 201 Created
- 500 Internal Error
- 404 Not Found
Answer: 404 Not Found. 404 Not Found indicates the resource doesn't exist.
Why do REST APIs commonly return JSON instead of HTML?
- It is compact and parseable by any language/client
- It is required by Flask
- It loads CSS automatically
- It is faster than all other formats by law
Answer: It is compact and parseable by any language/client. JSON is compact and universally parseable, so any client can consume the endpoint.
Continue this course
- Previous: Flashing Messages
- Next: JSON & Serialization — Convert Python objects to JSON and parse incoming JSON payloads
- Quick reference: Flask cheat sheet › REST & JSON
Frequently asked questions
What is a REST API?
A REST API is a web interface that lets programs exchange data over HTTP using standard verbs — GET to read, POST to create, PUT to update, and DELETE to remove. Instead of returning HTML pages, it returns structured data, usually JSON.
Which HTTP methods does a REST API use?
The core four map to CRUD operations: GET reads a resource, POST creates one, PUT (or PATCH) updates one, and DELETE removes one. In Flask you choose them with the methods argument on @app.route.
How do I return JSON in Flask?
Use jsonify(). Pass it a dictionary or list and Flask builds a proper JSON response with the right Content-Type header. You can also return a tuple like (jsonify(data), 201) to set the status code.
What status codes should a REST API return?
Return 200 for a successful read, 201 when you create something new, 204 for a successful delete with no body, 400 for bad input, and 404 when the resource is not found. Correct codes let clients react properly.