Skip to content
Tech

REST APIs Explained with Code Examples: Flask, Express and fetch

What REST really means, HTTP methods and status codes in tables, then the same books API built in Flask and Express, tested with curl and called with fetch.

Railway junction at dusk seen from above, with tracks branching and merging, glowing green and amber signal lamps and light trails from a train
Railway junction at dusk seen from above, with tracks branching and merging, glowing green and amber signal lamps and light trails from a train
In this article

    Almost every app you use talks to a server through an API, and most of those APIs follow the REST style: resources live at URLs, and you act on them with standard HTTP methods. This guide explains the ideas you need, shows the methods and status codes in tables, then builds the same small books API twice, once in Python with Flask and once in Node.js with Express. Finally it calls the API with curl and with JavaScript's fetch. All the code was run and every response shown below is real output.

    Versions and facts in this guide were checked against official documentation on 26 October 2026 (IST). Tools move fast, so confirm version numbers on the linked official pages before you install.

    What a REST API is

    REST (Representational State Transfer) is an architectural style, not a library or a standard you install. In practice, a "REST API" or "RESTful API" usually means:

    • Resources have URLs. /books is the collection of books; /books/2 is one book.
    • HTTP methods say what to do. GET reads, POST creates, PUT replaces, PATCH partially updates, DELETE removes.
    • Status codes say what happened. 200 OK, 201 Created, 404 Not Found and so on.
    • Data travels as JSON in request and response bodies, with a Content-Type: application/json header.
    • Each request is stateless. It carries everything the server needs (including authentication, such as a token header). The server does not rely on remembering earlier requests.

    HTTP methods

    MethodPurposeExampleSafe?Idempotent?
    GETRead a resource or listGET /booksYesYes
    POSTCreate a new resourcePOST /booksNoNo
    PUTReplace a resource completelyPUT /books/2NoYes
    PATCHChange part of a resourcePATCH /books/2NoNot necessarily
    DELETERemove a resourceDELETE /books/2NoYes

    Safe means the request should not change anything on the server. Idempotent means sending the same request twice has the same effect on the server as sending it once. Deleting book 2 twice leaves it deleted, while POSTing twice creates two books. These definitions come from the HTTP specification, RFC 9110. For making POST safe to retry, see our guide to idempotent API endpoints.

    Status codes you will use most

    CodeMeaningTypical use
    200 OKSuccess, with a bodyGET, PUT or PATCH that returns data
    201 CreatedA new resource was createdSuccessful POST
    204 No ContentSuccess, no bodySuccessful DELETE
    400 Bad RequestThe request is invalidMissing or malformed fields
    401 UnauthorizedNot authenticatedMissing or bad token
    403 ForbiddenAuthenticated, but not allowedUser lacks permission
    404 Not FoundNo such resourceUnknown ID
    409 ConflictClashes with current stateDuplicate or version conflict
    422 Unprocessable ContentWell-formed but semantically invalidUsed by some frameworks for validation errors
    500 Internal Server ErrorBug or failure on the serverUnhandled exception

    Classes: 2xx success, 3xx redirection, 4xx client error, 5xx server error. A consistent JSON error body (we use {"error": "..."}) makes clients much easier to write. Our article on designing clear API error responses goes deeper.

    Designing the books API

    Method and pathActionSuccessErrors
    GET /booksList all books200
    GET /books/:idGet one book200404
    POST /booksCreate a book201400
    PUT /books/:idReplace a book200400, 404
    DELETE /books/:idDelete a book204404

    Data is kept in memory to keep the focus on HTTP. A real API would use a database (see SQL basics) and authentication.

    Version 1: Python with Flask

    Install Flask (Flask 3.1.3 was the latest release on PyPI when we tested) in a virtual environment and save the file as app.py:

    python3 -m venv .venv
    source .venv/bin/activate   # Windows: .venv\Scripts\activate
    pip install flask
    # app.py - a minimal REST API for books (Flask 3)
    from flask import Flask, jsonify, request
    
    app = Flask(__name__)
    
    books = {
        1: {"id": 1, "title": "Wings of Fire", "author": "A. P. J. Abdul Kalam"},
        2: {"id": 2, "title": "The Guide", "author": "R. K. Narayan"},
    }
    next_id = 3
    
    
    @app.get("/books")
    def list_books():
        return jsonify(list(books.values()))
    
    
    @app.get("/books/<int:book_id>")
    def get_book(book_id):
        book = books.get(book_id)
        if book is None:
            return jsonify(error="Book not found"), 404
        return jsonify(book)
    
    
    @app.post("/books")
    def create_book():
        global next_id
        data = request.get_json(silent=True) or {}
        if not data.get("title") or not data.get("author"):
            return jsonify(error="title and author are required"), 400
        book = {"id": next_id, "title": data["title"], "author": data["author"]}
        books[next_id] = book
        next_id += 1
        return jsonify(book), 201
    
    
    @app.put("/books/<int:book_id>")
    def update_book(book_id):
        if book_id not in books:
            return jsonify(error="Book not found"), 404
        data = request.get_json(silent=True) or {}
        if not data.get("title") or not data.get("author"):
            return jsonify(error="title and author are required"), 400
        books[book_id] = {"id": book_id, "title": data["title"], "author": data["author"]}
        return jsonify(books[book_id])
    
    
    @app.delete("/books/<int:book_id>")
    def delete_book(book_id):
        if books.pop(book_id, None) is None:
            return jsonify(error="Book not found"), 404
        return "", 204
    
    
    if __name__ == "__main__":
        app.run(debug=True, port=5000)

    Run it with python app.py. The API listens on http://localhost:5000. The @app.get and @app.post decorators map methods and paths to functions, <int:book_id> converts the URL segment to an integer, and returning a tuple such as (body, 201) sets the status code. debug=True is for local development only.

    Version 2: Node.js with Express

    Create a project and install Express (5.3.0 was the latest when we tested):

    mkdir books-express && cd books-express
    npm init -y
    npm install express
    // server.js - the same books API in Express 5
    const express = require("express");
    
    const app = express();
    app.use(express.json());
    
    const books = new Map([
      [1, { id: 1, title: "Wings of Fire", author: "A. P. J. Abdul Kalam" }],
      [2, { id: 2, title: "The Guide", author: "R. K. Narayan" }],
    ]);
    let nextId = 3;
    
    app.get("/books", (req, res) => {
      res.json([...books.values()]);
    });
    
    app.get("/books/:id", (req, res) => {
      const book = books.get(Number(req.params.id));
      if (!book) return res.status(404).json({ error: "Book not found" });
      res.json(book);
    });
    
    app.post("/books", (req, res) => {
      const { title, author } = req.body ?? {};
      if (!title || !author) {
        return res.status(400).json({ error: "title and author are required" });
      }
      const book = { id: nextId++, title, author };
      books.set(book.id, book);
      res.status(201).json(book);
    });
    
    app.put("/books/:id", (req, res) => {
      const id = Number(req.params.id);
      if (!books.has(id)) return res.status(404).json({ error: "Book not found" });
      const { title, author } = req.body ?? {};
      if (!title || !author) {
        return res.status(400).json({ error: "title and author are required" });
      }
      const book = { id, title, author };
      books.set(id, book);
      res.json(book);
    });
    
    app.delete("/books/:id", (req, res) => {
      if (!books.delete(Number(req.params.id))) {
        return res.status(404).json({ error: "Book not found" });
      }
      res.status(204).end();
    });
    
    app.listen(3000, () => console.log("API running on http://localhost:3000"));

    Run it with node server.js. It listens on port 3000. express.json() parses JSON request bodies into req.body, and res.status(...).json(...) sets the status code and sends JSON. The ?? {} guard handles requests that arrive without a JSON body.

    Testing with curl

    These commands work against either server. Use port 5000 for Flask and 3000 for Express. The responses below are Express's (compact JSON). Flask returns the same data, pretty-printed in debug mode and with keys sorted alphabetically.

    # List all books
    curl -s http://localhost:3000/books
    [{"id":1,"title":"Wings of Fire","author":"A. P. J. Abdul Kalam"},{"id":2,"title":"The Guide","author":"R. K. Narayan"}]
    # A book that does not exist: look at the status line
    curl -s -i http://localhost:3000/books/42 | head -1
    HTTP/1.1 404 Not Found
    # Create a book (-w prints the status code)
    curl -s -X POST http://localhost:3000/books \
      -H "Content-Type: application/json" \
      -d '{"title": "Godan", "author": "Munshi Premchand"}' \
      -w ' %{http_code}\n'
    {"id":3,"title":"Godan","author":"Munshi Premchand"} 201
    # Missing author: validation error
    curl -s -X POST http://localhost:3000/books \
      -H "Content-Type: application/json" \
      -d '{"title": "No author"}' -w ' %{http_code}\n'
    {"error":"title and author are required"} 400
    # Replace book 2
    curl -s -X PUT http://localhost:3000/books/2 \
      -H "Content-Type: application/json" \
      -d '{"title": "The Guide", "author": "R.K. Narayan"}' -w ' %{http_code}\n'
    
    # Delete book 1, then try again
    curl -s -X DELETE http://localhost:3000/books/1 -o /dev/null -w '%{http_code}\n'
    curl -s -X DELETE http://localhost:3000/books/1 -w ' %{http_code}\n'
    {"id":2,"title":"The Guide","author":"R.K. Narayan"} 200
    204
    {"error":"Book not found"} 404

    The second DELETE returns 404 because the book is already gone, but the server's state is the same either way. That is idempotency in action. On Windows PowerShell, use curl.exe and adjust the quoting, or use a tool such as Postman or the VS Code REST Client extension.

    Calling the API with fetch

    fetch is built into every modern browser and into Node.js 18 and later. Save this as client.mjs:

    // client.mjs - call the API with fetch (Node 18+ or any modern browser)
    const BASE = process.env.API_URL ?? "http://localhost:5000";
    
    async function main() {
      // Create a book
      const res = await fetch(`${BASE}/books`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ title: "Malgudi Days", author: "R. K. Narayan" }),
      });
      if (!res.ok) throw new Error(`POST failed: ${res.status}`);
      const created = await res.json();
      console.log("Created:", res.status, created);
    
      // List all books
      const list = await fetch(`${BASE}/books`).then((r) => r.json());
      console.log("Total books:", list.length);
    
      // Ask for a book that does not exist
      const missing = await fetch(`${BASE}/books/999`);
      console.log("Missing book status:", missing.status, await missing.json());
    }
    
    main().catch((err) => console.error(err));

    Running node client.mjs against the Flask server printed:

    Created: 201 { author: 'R. K. Narayan', id: 4, title: 'Malgudi Days' }
    Total books: 3
    Missing book status: 404 { error: 'Book not found' }

    Two things trip up beginners. First, fetch only rejects on network failures. A 404 or 500 still resolves, so check res.ok or res.status yourself, as the code does. Second, if you call the API from a web page served on a different origin (a different port counts), the browser enforces CORS. The server must send an Access-Control-Allow-Origin header, for example via the flask-cors package or the cors middleware for Express. curl and Node scripts are not affected.

    Flask vs Express for this API

    Flask (Python)Express (Node.js)
    RoutingDecorators: @app.get("/books")Methods: app.get("/books", handler)
    URL parameters<int:book_id>, converted for you:id, a string you convert
    JSON bodyrequest.get_json()express.json() middleware, then req.body
    Setting statusReturn (body, 201)res.status(201).json(body)
    Production serverA WSGI server such as gunicornNode itself, often behind a reverse proxy

    If you prefer type hints and automatic OpenAPI docs in Python, FastAPI is a popular alternative to Flask. The REST concepts are identical.

    Next steps

    • Package the API in a container with our Docker basics guide, which builds a Flask API backed by PostgreSQL.
    • Replace the in-memory dictionary with a real database table, and add paging (limit and offset, or cursors) once lists grow large.
    • New to Python or JavaScript? Start with Python vs JavaScript vs Java.

    FAQ

    Is REST the same as HTTP?

    No. HTTP is the protocol; REST is a design style that uses HTTP's methods, URLs and status codes in a consistent way.

    When should I use PUT vs PATCH?

    PUT replaces the whole resource with what you send. PATCH applies a partial change. Our example uses PUT and requires both title and author.

    REST vs GraphQL?

    REST exposes several resource URLs. GraphQL usually exposes one endpoint where the client asks for exactly the fields it needs. REST is simpler to start with and is still the most common style.

    Why does my browser call fail but curl works?

    Almost always CORS. Browsers block cross-origin responses unless the server allows them with Access-Control-Allow-Origin headers.

    Sources

    Written by Raj

    Founder & Editor

    I write HorizonDesk: practical, no-hype articles on technology, money, business and the vehicles we drive. Everything here is something I’ve researched, tested or wanted explained clearly myself.