---
title: "Your First API with FastAPI: Products, Input Checks and Automatic Docs"
url: https://phatdev.net/en/blog/python-fastapi-first-api
lang: en
updated: 2026-10-21
alternate:
  vi: https://phatdev.net/vi/blog/python-fastapi-first-api
markdown: https://phatdev.net/en/blog/python-fastapi-first-api.md
---

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

Categories: Backend | Published: 2026-10-21 | Level: Beginner | 7 min read

Tags: python, fastapi, pydantic, api, curl, openapi

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 sends a request to a server holding a shelf of products and gets a response](https://phatdev.net/uploads/blog/python-fastapi-first-api/request-1920.webp)

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: well-formed packets pass, bad ones are sent back](https://phatdev.net/uploads/blog/python-fastapi-first-api/validate-1920.webp)

## Setup

You need Python and uv; see [Getting Started with Python in 2026: Install, a Clean Setup and Your First Script](https://phatdev.net/en/blog/python-start-2026) if you do not have them yet. Following the FastAPI docs, the standard install is:

```bash
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.

```python
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:

```bash
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:

```bash
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:

```json
{"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:

```bash
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:

```bash
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:

```json
{"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.

![Source code producing a documentation panel with try-it buttons](https://phatdev.net/uploads/blog/python-fastapi-first-api/docs-1920.webp)

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](https://phatdev.net/en/blog/rest-api-design-practical) 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#](https://phatdev.net/en/blog/jwt-across-languages) 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.

## References

- [FastAPI tutorial](https://fastapi.tiangolo.com/tutorial/)
- [Pydantic documentation](https://docs.pydantic.dev/latest/)
- [uv projects guide (Astral)](https://docs.astral.sh/uv/guides/projects/)
- [OpenAPI Specification](https://spec.openapis.org/oas/latest.html)

## Related posts

- [Getting Started with Python in 2026: Install, a Clean Setup and Your First Script](https://phatdev.net/en/blog/python-start-2026.md)
- [Designing an API Other People Will Use: Errors, Paging, Safe Retries and Limits](https://phatdev.net/en/blog/rest-api-design-practical.md)
- [A Shared JWT Login Token for Node.js, PHP, Python and C#](https://phatdev.net/en/blog/jwt-across-languages.md)
