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

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:

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.

TaskCode
Return JSONreturn jsonify(data)
Read request bodyrequest.get_json()
Created statusreturn jsonify(item), 201
Accept POSTmethods=["POST"]

🏆 Mini Challenge: A Notes API

Build a tiny notes API that supports reading all notes and deleting one by id.

# 🏆 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

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.

Related lessons