Your First API with FastAPI: Products, Input Checks and Automatic Docs

When every system keeps its own stock copy, one is always wrong. Build a small FastAPI service, validate input with Pydantic, watch the docs appear by themselves and try it with curl.

Phát LâmPublished 7 min readBeginner
Your First API with FastAPI: Products, Input Checks and Automatic Docs
On this page
  1. An API is like a service counter
  2. Setup
  3. For those who write code
  4. Try it with curl
  5. What if the data is wrong
  6. The docs appear on their own
  7. Common problems
  8. Next steps

A shop has a stock spreadsheet. The website reads numbers from somewhere. The point-of-sale app at the counter reads from somewhere else. Each place keeps its own copy, and one of them is always wrong. The cleanest way out is to have one place that holds the product list, and let everything else ask it. That place is called an API.

API (a way for programs to talk to each other) sounds distant, but a small one in Python takes about 50 lines. This post builds a product-management API with FastAPI, runs it for real, calls it with curl, and shows you a documentation page that appears on its own without you writing a word.

An API is like a service counter

Picture the counter at a cafe. You hand over a slip that says clearly: "add one drink", "show me the menu", "remove this item". The staff does it and hands back a reply slip: done, or we do not have that. If the slip is nonsense ("one drink at minus five dollars"), the staff refuses on the spot and says what is wrong.

An API works the same way. The order slip is a request sent over the network. The reply slip is a response, with a number that tells the result: 200 is fine, 201 is just created, 404 is not found, 422 is invalid data.

A client device sends a data packet along a glowing line to a server block with a shelf of product boxes, and a reply returns on a second line
A request goes out, a response comes back.

The job of "checking the order slip" belongs to Pydantic, the data-checking library FastAPI uses out of the box. You declare once "a product needs a name of at least 2 characters and a price above 0", and it rejects everything that does not fit.

A checkpoint gate where well-formed packets pass and bent ones are bounced back
Pydantic stops bad data before it reaches your code.

Setup

You need Python and uv; see Getting Started with Python in 2026: Install, a Clean Setup and Your First Script if you do not have them yet. Following the FastAPI docs, the standard install is:

mkdir stock-api && cd stock-api
uv init --bare --python 3.15
uv add "fastapi[standard]"

I checked on 21 October 2026: FastAPI 0.156.4 (requires Python 3.10 or newer), Pydantic 2.14.1, Uvicorn 0.54.0. The [standard] bundle includes the fastapi command to run the server.

For those who write code

Save main.py. The data lives in memory to keep the example short; in a real project you would use a database. The Vietnamese names in the file are kept on purpose so the code matches what I ran.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="Kho hàng nhỏ", version="1.0.0")


class SanPhamMoi(BaseModel):
    ten: str = Field(min_length=2, max_length=100, description="Tên sản phẩm")
    gia: int = Field(gt=0, description="Giá bán, đơn vị đồng")
    ton_kho: int = Field(default=0, ge=0)


class SanPham(SanPhamMoi):
    id: int


kho: dict[int, SanPham] = {}
id_tiep_theo = 1


@app.get("/san-pham", response_model=list[SanPham])
def danh_sach(gia_toi_da: int | None = None):
    ds = list(kho.values())
    if gia_toi_da is not None:
        ds = [sp for sp in ds if sp.gia <= gia_toi_da]
    return ds


@app.post("/san-pham", response_model=SanPham, status_code=201)
def them(sp: SanPhamMoi):
    global id_tiep_theo
    moi = SanPham(id=id_tiep_theo, **sp.model_dump())
    kho[moi.id] = moi
    id_tiep_theo += 1
    return moi


@app.get("/san-pham/{sp_id}", response_model=SanPham)
def xem(sp_id: int):
    if sp_id not in kho:
        raise HTTPException(status_code=404, detail="Không có sản phẩm này")
    return kho[sp_id]


@app.put("/san-pham/{sp_id}", response_model=SanPham)
def sua(sp_id: int, sp: SanPhamMoi):
    if sp_id not in kho:
        raise HTTPException(status_code=404, detail="Không có sản phẩm này")
    kho[sp_id] = SanPham(id=sp_id, **sp.model_dump())
    return kho[sp_id]


@app.delete("/san-pham/{sp_id}", status_code=204)
def xoa(sp_id: int):
    if kho.pop(sp_id, None) is None:
        raise HTTPException(status_code=404, detail="Không có sản phẩm này")

There are two product classes for a simple reason: when adding a new product, the caller must not choose the id; the server assigns it. SanPhamMoi ("new product") is the order slip, SanPham ("product") is the item once recorded.

Start the server in development mode:

uv run fastapi dev --port 8010

The terminal reports the server at http://127.0.0.1:8010. This mode reloads itself whenever you edit the file, which is handy while learning. For a real deployment use fastapi run, as the docs advise.

Try it with curl

Open another terminal. Add the first product:

curl -i -X POST http://127.0.0.1:8010/san-pham \
  -H 'Content-Type: application/json' \
  -d '{"ten":"Chuột không dây","gia":190000,"ton_kho":12}'

I got HTTP/1.1 201 Created and this body:

{"ten":"Chuột không dây","gia":190000,"ton_kho":12,"id":1}

Add a second one, "Bàn phím cơ" at 650000, without stock; the server fills ton_kho with 0. View the list and filter by price:

curl http://127.0.0.1:8010/san-pham
curl "http://127.0.0.1:8010/san-pham?gia_toi_da=300000"

The second command returns only the mouse, because the keyboard costs more than 300000. Updating and deleting work the same way: curl -X PUT .../san-pham/1 -H 'Content-Type: application/json' -d '{...}' returns the updated item, and curl -X DELETE .../san-pham/2 returns 204 with no body.

What if the data is wrong

Send a one-letter name and a negative price:

curl -i -X POST http://127.0.0.1:8010/san-pham \
  -H 'Content-Type: application/json' -d '{"ten":"A","gia":-5}'

The response is 422 with a clear explanation of each problem, and you wrote no checking code:

{"detail":[{"type":"string_too_short","loc":["body","ten"],"msg":"String should have at least 2 characters",...},
 {"type":"greater_than","loc":["body","gia"],"msg":"Input should be greater than 0",...}]}

Send "gia":"rẻ" (a word as the price) and you get an int_parsing error saying the price must be a whole number. Ask for product 99, which does not exist, and you get 404 with "Không có sản phẩm này".

The docs appear on their own

Open http://127.0.0.1:8010/docs in a browser. FastAPI builds an interactive documentation page: every path, the fields to send, example responses, and a "Try it out" button that calls the API right there. The /redoc page is another view of the same information. I checked that both return 200, and /openapi.json reports the OpenAPI 3.1.0 standard.

A block of code generating a documentation panel with switches, a run button and rounded endpoint bars
Write the code and the documentation page appears.

This is more useful than it sounds. Someone building a web front end or a mobile app reads this page and knows how to call you, with no need to ask question after question. The post Designing an API Other People Will Use: Errors, Paging, Safe Retries and Limits goes further on making an API easier to use.

Common problems

First: data disappears on restart. Because it lives in memory, stopping the server wipes it, and in dev mode just saving the file reloads the app and forgets everything. Use a database when you need to keep data.

Second: forgetting the Content-Type: application/json header in curl. The server cannot tell what you sent and answers 422. On Windows PowerShell, quotes and the \ line continuation differ, so use double quotes and put the command on one line.

Third: anyone can call this API. The sample has no login. Before putting it on the internet you need authentication, and A Shared JWT Login Token for Node.js, PHP, Python and C# shows one way to do it.

Next steps

You now have a working API. The most sensible next steps are replacing the in-memory list with a database, adding login, then trying to call the API from a web page or your own spreadsheet. If your shop needs an API that connects the website, marketplaces and stock, send me a message through the contact page.

PythonREST APIpythonfastapipydanticapicurlopenapi

References

  1. FastAPI tutorial
  2. Pydantic documentation
  3. uv projects guide (Astral)
  4. OpenAPI Specification