Tutorial Python March 14, 2026

How to Build and Test a REST API From Scratch (2026 Guide)

REST APIs power nearly every modern application — from mobile apps and single-page frontends to the wave of AI agents that now consume APIs autonomously. This guide walks you through building a fully functional REST API with Python and FastAPI, then testing it properly. No fluff, just working code you can ship.

What Is a REST API?

REST (Representational State Transfer) is an architectural style for building web services. A REST API exposes resources — things like users, products, or orders — at predictable URLs and lets clients interact with them using standard HTTP methods.

What makes an API "RESTful" in practice:

HTTP Methods: The Foundation

Every REST API operation maps to an HTTP method. Here's how they correspond to CRUD operations:

Method CRUD Operation Example Idempotent
GET Read GET /books Yes
POST Create POST /books No
PUT Update PUT /books/1 Yes
DELETE Delete DELETE /books/1 Yes

Idempotent means calling the same request multiple times produces the same result. GET, PUT, and DELETE are idempotent; POST is not — sending the same POST twice creates two resources.

Why FastAPI in 2026

Flask dominated Python API development for a decade, but FastAPI has become the clear default for new projects. The reasons are practical:

Building a REST API Step by Step

We'll build a book catalog API with full CRUD operations. Start by installing FastAPI and Uvicorn:

pip install fastapi uvicorn

Project Structure

book-api/
├── main.py          # API routes and app setup
├── models.py        # Pydantic data models
└── requirements.txt # Dependencies

Define Your Data Models

Pydantic models define the shape of your data. They handle validation automatically — if a client sends an invalid request body, FastAPI returns a clear 422 error before your code ever runs.

# models.py
from pydantic import BaseModel, Field
from typing import Optional

class BookCreate(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    author: str = Field(..., min_length=1, max_length=100)
    year: int = Field(..., ge=1000, le=2030)
    isbn: Optional[str] = Field(None, pattern=r"^\d{10}(\d{3})?$")

class Book(BookCreate):
    id: int

class BookUpdate(BaseModel):
    title: Optional[str] = Field(None, min_length=1, max_length=200)
    author: Optional[str] = Field(None, min_length=1, max_length=100)
    year: Optional[int] = Field(None, ge=1000, le=2030)
Tip — Input validation with regex: The isbn field uses a regex pattern to enforce either 10 or 13-digit ISBN formats. Need to build more complex validation patterns? Generate them instantly with our free Regex Generator — describe what you want to match in plain English and get the pattern back.

Create the API Routes

# main.py
from fastapi import FastAPI, HTTPException
from models import Book, BookCreate, BookUpdate
from typing import List

app = FastAPI(
    title="Book Catalog API",
    description="A simple REST API for managing books",
    version="1.0.0"
)

# In-memory storage (use a database in production)
books_db: dict[int, Book] = {}
next_id = 1


@app.get("/books", response_model=List[Book])
def list_books(author: str = None, year: int = None):
    """List all books, with optional author/year filters."""
    results = list(books_db.values())
    if author:
        results = [b for b in results if author.lower() in b.author.lower()]
    if year:
        results = [b for b in results if b.year == year]
    return results


@app.get("/books/{book_id}", response_model=Book)
def get_book(book_id: int):
    """Get a single book by ID."""
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    return books_db[book_id]


@app.post("/books", response_model=Book, status_code=201)
def create_book(book: BookCreate):
    """Create a new book entry."""
    global next_id
    new_book = Book(id=next_id, **book.model_dump())
    books_db[next_id] = new_book
    next_id += 1
    return new_book


@app.put("/books/{book_id}", response_model=Book)
def update_book(book_id: int, updates: BookUpdate):
    """Update an existing book (partial updates supported)."""
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    existing = books_db[book_id]
    update_data = updates.model_dump(exclude_unset=True)
    updated = existing.model_copy(update=update_data)
    books_db[book_id] = updated
    return updated


@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    """Delete a book by ID."""
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail="Book not found")
    del books_db[book_id]

Run the server:

uvicorn main:app --reload --port 8000

Your API is now live at http://localhost:8000. Visit /docs for the interactive Swagger UI, or /openapi.json for the raw schema that AI agents can consume.

Testing Your API

Building an API is half the work. Testing it properly is the other half — and where most tutorials cut corners. Here are the three layers of testing you should use.

Manual Testing: Quick Smoke Checks

Before writing automated tests, verify each endpoint works. You can do this instantly in a browser with our free API Tester — enter your local URL, pick the HTTP method, and send. No signup, no install.

Or use curl from the terminal:

# Create a book
curl -X POST http://localhost:8000/books \
  -H "Content-Type: application/json" \
  -d '{"title": "Dune", "author": "Frank Herbert", "year": 1965}'

# List all books
curl http://localhost:8000/books

# Get a specific book
curl http://localhost:8000/books/1

# Update a book
curl -X PUT http://localhost:8000/books/1 \
  -H "Content-Type: application/json" \
  -d '{"year": 1965, "title": "Dune (Revised)"}'

# Delete a book
curl -X DELETE http://localhost:8000/books/1

Test your endpoints with our free API Tester — paste a URL, pick a method, hit send. Zero friction.

Open API Tester

Automated Testing with pytest

FastAPI includes a TestClient that lets you write tests without starting a real server. Install the test dependencies:

pip install pytest httpx

Then write your tests:

# test_api.py
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_create_book():
    response = client.post("/books", json={
        "title": "Neuromancer",
        "author": "William Gibson",
        "year": 1984
    })
    assert response.status_code == 201
    data = response.json()
    assert data["title"] == "Neuromancer"
    assert "id" in data

def test_get_nonexistent_book():
    response = client.get("/books/9999")
    assert response.status_code == 404

def test_create_book_invalid_year():
    response = client.post("/books", json={
        "title": "Future Book",
        "author": "Someone",
        "year": 3000  # Exceeds max of 2030
    })
    assert response.status_code == 422

def test_filter_by_author():
    client.post("/books", json={
        "title": "Count Zero",
        "author": "William Gibson",
        "year": 1986
    })
    response = client.get("/books?author=gibson")
    assert response.status_code == 200
    books = response.json()
    assert all("Gibson" in b["author"] for b in books)

Run with pytest test_api.py -v. Every endpoint should have at least a happy path and a failure case.

Health Checks and Monitoring

Production APIs need a health endpoint. Add one to your app:

@app.get("/health")
def health_check():
    return {"status": "healthy", "version": "1.0.0"}

Then schedule automated health checks to run on a cron schedule. A simple approach: hit the /health endpoint every 5 minutes and alert if it fails. Need to build the right cron expression? Our Cron Expression Generator can produce the schedule in crontab, GitHub Actions, Kubernetes, or systemd format with one click.

# crontab entry: check API health every 5 minutes
*/5 * * * * curl -sf http://localhost:8000/health > /dev/null || echo "API DOWN" | mail -s "Alert" you@email.com

Error Handling Best Practices

Good error responses help clients (human and AI) recover gracefully. Follow these conventions:

FastAPI handles validation errors automatically, but for custom errors, use exception handlers:

from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    return JSONResponse(
        status_code=500,
        content={
            "detail": "Internal server error",
            "type": type(exc).__name__
        }
    )

REST API Best Practices for 2026

These conventions are well-established, but they matter more now that AI agents are consuming APIs alongside humans:

URL Design

API Versioning

Version your API from day one. The simplest approach is URL-based:

app = FastAPI(root_path="/v1")

# Endpoints become /v1/books, /v1/books/{id}, etc.

When you need breaking changes, deploy /v2 alongside /v1 and give consumers time to migrate.

Pagination

Never return unbounded lists. Add pagination to any endpoint that returns collections:

@app.get("/books", response_model=List[Book])
def list_books(skip: int = 0, limit: int = 20):
    all_books = list(books_db.values())
    return all_books[skip : skip + limit]

AI-Agent Readiness

In 2026, a growing percentage of API traffic comes from AI agents — LLMs that autonomously discover and call endpoints. To make your API agent-friendly:

Quick Deployment Checklist

Before shipping to production:

  1. Add CORS middleware if browsers will call your API directly
  2. Enable HTTPS — never run a production API over plain HTTP
  3. Add rate limiting to prevent abuse (especially with AI agent traffic)
  4. Set up logging — structured JSON logs that include request IDs
  5. Write a /health endpoint and monitor it on a cron schedule
  6. Validate all inputs — use regex patterns for strings like emails, ISBNs, and phone numbers
  7. Test every endpoint — manually with our API Tester, then automated with pytest

What's Next

You now have a working REST API with CRUD operations, validation, error handling, and tests. From here, the natural next steps are connecting a real database (SQLite with SQLAlchemy, or PostgreSQL with asyncpg), adding authentication (OAuth2 or API keys), and deploying behind a reverse proxy like Nginx.

The core patterns don't change. Whether your API serves a React frontend, a mobile app, or an AI agent that discovers your endpoints through the OpenAPI schema — the same principles apply: predictable URLs, proper status codes, validated inputs, and clear error messages.

Ready to test your API? Try these free developer tools — no signup required.

API Tester Regex Generator Cron Generator