Build Your First REST API: A Beginner's Roadmap
Design and build a working REST API in Python with FastAPI: endpoints, Pydantic models, status codes, and curl tests, explained step by step.
Building your first REST API is one of those milestones that changes how you see the web. Suddenly every app on your phone makes sense: it is a pretty screen talking to an API exactly like the one you are about to build.
This guide takes you from zero to a small, working API in Python. We will design it on paper first, implement it with FastAPI, and test it from the command line. If the word "API" itself is still fuzzy, read What is an API? first; this article picks up where that one ends.
What a REST API is in practice
Strip away the jargon and a REST API is three ideas working together.
Resources. Your API exposes things: users, products, notes. Each type of thing lives at a URL called an endpoint, like /notes for the collection and /notes/3 for one specific note.
HTTP verbs. The URL says what you are talking about; the HTTP method says what you want to do with it:
| Verb | Meaning | Everyday analogy |
|---|---|---|
GET | Read data | Looking at a page |
POST | Create something new | Submitting a form |
PUT / PATCH | Update something | Editing a saved draft |
DELETE | Remove something | Emptying the trash |
Status codes. Every response carries a three-digit code that says how it went. You only need a handful at first: 200 means OK, 201 means created, 204 means done with nothing to return, 404 means not found, 422 means your input failed validation, and 500 means the server itself broke.
That is the whole contract. A client sends a verb plus a URL (and maybe a JSON body), the server replies with a status code (and maybe a JSON body). Everything else, auth, caching, versioning, is refinement layered on top of this loop.
Design first: a tiny notes API on paper
Here is a habit that will serve you for your entire career: write down your endpoints before you write any code. It costs five minutes and catches design mistakes while they are still free to fix.
Our project is deliberately small: a personal notes API. Each note has an id, a title, and a body. On paper, the API looks like this:
| Action | Method | Path | Request body | Success response |
|---|---|---|---|---|
| List all notes | GET | /notes | none | 200, array of notes |
| Get one note | GET | /notes/{id} | none | 200, one note |
| Create a note | POST | /notes | { "title", "body" } | 201, the new note |
| Delete a note | DELETE | /notes/{id} | none | 204, empty |
Notice the conventions doing quiet work here. The collection lives at a plural noun, /notes. A single item is the collection path plus an ID. The verbs carry the action, so there is no /getNotes or /deleteNote in the paths. Clients that know one REST API can guess their way around yours, and that predictability is most of what "RESTful" means in practice.
We are skipping update (PUT/PATCH) to keep the walkthrough tight, but once you finish, adding it yourself is the perfect exercise.
Set up FastAPI
We will build this in Python with FastAPI, which is the friendliest serious choice for a first API: modern, fast, and it generates interactive documentation for free.
You need Python 3.10 or newer. In a fresh folder, create a virtual environment and install FastAPI with its standard extras, which include the development server:
python -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
pip install "fastapi[standard]"
That is the entire setup. One package, one command.
The app and your first endpoint
Create a file called main.py. We will start with the app object, the data models, and the list endpoint:
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
app = FastAPI(title="Notes API")
class NoteIn(BaseModel):
"""What the client sends when creating a note."""
title: str = Field(min_length=1, max_length=100)
body: str = ""
class Note(NoteIn):
"""A stored note: everything the client sent, plus an id."""
id: int
# Our "database" for now: a plain dict in memory
notes: dict[int, Note] = {}
next_id = 1
@app.get("/notes")
def list_notes() -> list[Note]:
return list(notes.values())
Two things to notice. First, the models: NoteIn describes what clients are allowed to send, and Note is what we store and return. Keeping input and output models separate is a habit that pays off the moment your stored data grows fields clients should never set themselves, like id or created_at.
Second, the decorator: @app.get("/notes") is the line that binds an HTTP verb and a path to a Python function. That one-to-one mapping between rows in your paper design and decorated functions is the core rhythm of FastAPI.
Run it with the development server:
fastapi dev main.py
Open http://127.0.0.1:8000/notes in your browser and you will see [], an empty JSON list. Your API is alive. Also visit http://127.0.0.1:8000/docs: FastAPI has already generated interactive documentation from your code, and it will update itself as you add endpoints.
GET one note, and your first 404
Reading a single note introduces two new ideas: path parameters and error responses.
@app.get("/notes/{note_id}")
def get_note(note_id: int) -> Note:
note = notes.get(note_id)
if note is None:
raise HTTPException(status_code=404, detail="Note not found")
return note
The {note_id} in the path becomes the note_id argument of the function, and the int type hint does real work: FastAPI converts the URL segment to an integer and automatically rejects requests like /notes/abc with a validation error before your code even runs.
The 404 matters more than it looks. A beginner instinct is to return null or an empty object when something is missing. Resist it. The status code is the answer, and every HTTP client library on earth knows how to handle a 404 correctly.
POST: creating notes with a Pydantic model
Now the endpoint that makes the API useful:
@app.post("/notes", status_code=status.HTTP_201_CREATED)
def create_note(data: NoteIn) -> Note:
global next_id
note = Note(id=next_id, **data.model_dump())
notes[next_id] = note
next_id += 1
return note
Because the function accepts a NoteIn parameter, FastAPI knows to read the request body as JSON and validate it against the model. Send a note with no title, a title over 100 characters, or a body that is not a string, and the client gets a detailed 422 response without you writing a single if statement. Declarative validation like this is the single biggest reason to use a framework instead of parsing JSON by hand.
We also declare status_code=status.HTTP_201_CREATED, because creating something and merely reading it are different events, and good APIs tell the truth about which one happened.
DELETE: removing a note
@app.delete("/notes/{note_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_note(note_id: int) -> None:
if note_id not in notes:
raise HTTPException(status_code=404, detail="Note not found")
del notes[note_id]
204 No Content is the conventional reply to a successful delete: the operation worked and there is nothing meaningful to send back. Note that deleting a missing note is still a 404; being honest about "that never existed" beats pretending the delete succeeded.
That completes every row of the paper design. The whole API is about 40 lines.
Test it with curl
The browser only sends GET requests, so for the rest we will use curl, the command-line HTTP client that every backend developer ends up knowing. With the server still running, open a second terminal:
# Create two notes
curl -X POST http://127.0.0.1:8000/notes \
-H "Content-Type: application/json" \
-d '{"title": "First note", "body": "Hello, API"}'
curl -X POST http://127.0.0.1:8000/notes \
-H "Content-Type: application/json" \
-d '{"title": "Shopping", "body": "coffee, oats"}'
# List everything
curl http://127.0.0.1:8000/notes
# Get note 1
curl http://127.0.0.1:8000/notes/1
# Delete note 1, showing the status line
curl -i -X DELETE http://127.0.0.1:8000/notes/1
# Prove it is gone (expect a 404)
curl -i http://127.0.0.1:8000/notes/1
Walk through the whole sequence and watch the status codes: 201, 200, 200, 204, 404. If they match, you have built and verified a working REST API. Try the failure cases too: POST a note with an empty title and read the 422 response carefully. Learning to read error bodies now will save you hours later.
Where the data really lives
Restart the server and list your notes again. They are gone.
That is not a bug; it is the honest consequence of our notes dict living in the server's memory. For learning, that is exactly right: you get the full create-read-delete loop with zero moving parts.
A real API swaps the dict for a database (PostgreSQL and SQLite are the usual first choices) so data survives restarts, handles many users at once, and can be queried efficiently. The beautiful part of how we structured this: your endpoints, models, status codes, and tests all stay the same. Only the storage lines change, typically to a library like SQLModel or SQLAlchemy. The API is the contract; the database is an implementation detail behind it.
What production adds
Everything above is a genuine REST API, but a public one needs a few more layers. You do not need to build these today; you do need to know they exist:
- Authentication and authorization. Who is calling, and which notes are they allowed to touch? Usually API keys or JWT tokens, plus per-user filtering in every query.
- Deeper validation and limits. Maximum body sizes, pagination on list endpoints, and stricter rules than "title under 100 characters."
- Rate limiting. A cap on requests per minute per client, so one buggy script or hostile user cannot flatten your server.
- HTTPS, logging, and monitoring. Encrypted transport, a record of what happened, and an alarm when error rates climb.
Each of these is its own satisfying rabbit hole, and they all bolt onto the same foundation you just built.
Frequently asked questions
Why FastAPI instead of Flask or Django?
All three are solid. FastAPI earns the recommendation for a first API because the type hints you write anyway become validation and live documentation, so beginners get professional behavior by default. Flask is smaller but leaves validation to you; Django is a full web framework where the API part is one piece of a much bigger machine.
Do I need to memorize all the status codes?
No. Fluency with about six (200, 201, 204, 404, 422, 500) covers daily work, and you can look up the rest when an API you consume returns something exotic. What matters is the habit of checking the status code instead of assuming success.
What is the difference between REST and HTTP?
HTTP is the transport protocol: verbs, headers, status codes. REST is a set of conventions for using HTTP well: model things as resources at predictable URLs, use the verbs for their intended meanings, and keep requests stateless. Every REST API speaks HTTP; not every HTTP service follows REST conventions.
How do I put my API on the internet?
Platforms like Railway, Render, or Fly.io can deploy a FastAPI app from a GitHub repo in minutes. Before you do, add the production layers above, or at minimum authentication, because anything public gets probed by bots within hours. Deploy after the fundamentals feel solid, not before.
Your next step
You designed an API on paper, implemented all four endpoints, and verified every status code from the command line. That loop, design, build, test, is the actual job of backend development; everything senior engineers add is refinement of it.
The natural next moves: add a PUT /notes/{id} endpoint yourself, then swap the dict for SQLite. If you want that path as guided, interactive lessons, from these fundamentals through databases and authentication, the FastAPI & REST APIs course picks up exactly where this article leaves off.
Kodion Team
Kodion Editorial
Written by the team that builds Kodion's courses: working engineers who test every example in the interactive editor before it ships. How we create and review content
Continue Learning
π Related Articles
Python List Comprehensions, Step by Step
Learn Python list comprehensions step by step: turn any loop into one clean line, filter with if, and know exactly when a plain loop is the better choice.
Recursion Explained Without the Headache
Understand recursion with a step-by-step call stack walk-through of factorial, real Python examples, and clear rules for when a loop is the better tool.