FastAPI - REST

  • Un servidor web es pot configurar com un servidor de recursos que s'accedeixen mijançant un conjunt de funcions "externes" o endpoints (API)

    Entorn de treball

    Crea un projecte amb uv

    Terminal window
    uv init python-rest
    cd python-rest

    El primer pas és instal·lar FastAPI.

    Terminal window
    uv add "fastapi[standard]"
    Nota

    Quan instal·les amb uv add "fastapi[standard]" ve amb algunes dependències estàndard opcionals per defecte, incloent fastapi-cloud-cli, que et permet desplegar a FastAPI Cloud.

    Si no vols tenir aquestes dependències opcionals, pots instal·lar en canvi uv add fastapi.

    Si vols instal·lar les dependències estàndard però sense el fastapi-cloud-cli, pots instal·lar amb uv add "fastapi[standard-no-fastapi-cloud-cli]"

    Primers passos

    The app variable is the “instance” of the class FastAPI and will be the main point of interaction to create all your API.

    main.py
    from fastapi import FastAPI
    app = FastAPI()
    @app.get("/")
    async def root():
    return {"message": "Hello World"}

    Executa el servidor amb fastapi dev:

    Terminal window
    fastapi dev main.py
    Starting development server 🚀
    Server started at http://127.0.0.1:8000
    Documentation at http://127.0.0.1:8000/docs

    Obre el teu navegador a http://127.0.0.1:8000.

    Veuràs la resposta JSON com:

    {"message": "Hello World"}

    OpenAPI

    FastAPI genera un “schema” amb tota la teva API utilitzant l’estàndard OpenAPI per definir APIs.

    Ves a http://127.0.0.1:8000/docs.

    Veuràs la documentació interactiva automàtica de l’API (proporcionada per Swagger UI)

    I ara, ves a http://127.0.0.1:8000/redoc.

    Veuràs la documentació automàtica alternativa (proporcionada per ReDoc).

    • Schema. Un “schema” és una definició o descripció d’alguna cosa. No el codi que ho implementa, sinó només una descripció abstracta.

    • API “schema”. En aquest cas, OpenAPI és una especificació que dicta com definir un schema de la teva API. Aquesta definició del schema inclou els paths de la teva API, els possibles paràmetres que accepten, etc.

    • Data “schema”. El terme “schema” també pot referir-se a la forma d’algunes dades, com un contingut JSON. En aquest cas, significaria els atributs JSON i els tipus de dades que tenen, etc.

    OpenAPI defineix un schema d’API per a la teva API. I aquest schema inclou definicions (o “schemas”) de les dades enviades i rebudes per la teva API utilitzant JSON Schema, l’estàndard per schemas de dades JSON.

    Si tens curiositat per veure com és el schema OpenAPI en cru, FastAPI genera automàticament un JSON (schema) amb les descripcions de tota la teva API.

    Pots veure-ho directament a: http://127.0.0.1:8000/openapi.json.

    Mostrarà un JSON que comença amb alguna cosa com:

    {
    "openapi": "3.1.0",
    "info": {
    "title": "FastAPI",
    "version": "0.1.0"
    },
    "paths": {
    "/items/": {
    "get": {
    "responses": {
    "200": {
    "description": "Successful Response",
    "content": {
    "application/json": {
    ...

    Per a què serveix OpenAPI ?

    El schema OpenAPI és el que alimenta els dos sistemes de documentació interactiva inclosos.

    I hi ha dotzenes d’alternatives, totes basades en OpenAPI. Pots afegir fàcilment qualsevol d’aquestes alternatives a la teva aplicació construïda amb FastAPI.

    També pots utilitzar-lo per generar codi automàticament, per a clients que es comuniquen amb la teva API. Per exemple, aplicacions frontend, mòbils o IoT.

    Decorador d’operació de path

    main.py
    @app.get("/")
    async def root():
    return {"message": "Hello World"}

    Path

    “Path” aquí es refereix a l’última part de l’URL començant des del primer /.

    Així, en un URL com https://xtec.dev/item/4 el path seria /item/4.

    Nota

    Un “path” també es coneix habitualment com a “endpoint” o “ruta”.

    Operació

    “Operació” aquí es refereix a un dels “mètodes” HTTP.

    Un de: POST, GET, PUT, DELETE.

    … i els més exòtics: PATCH, OPTIONS, HEAD, TRACE

    En el protocol HTTP, pots comunicar-te amb cada path utilitzant un (o més) d’aquests “mètodes”.

    Quan construeixes APIs, normalment utilitzes aquests mètodes HTTP específics per realitzar una acció específica.

    Normalment utilitzes:

    • POST: per crear dades.
    • GET: per llegir dades.
    • PUT: per actualitzar dades.
    • DELETE: per esborrar dades.

    Així, a OpenAPI, cada un dels mètodes HTTP s’anomena una “operació”.

    Defineix un decorador d’operació de path

    main.py
    @app.get("/")
    async def root():
    return {"message": "Hello World"}

    El @app.get("/") indica a FastAPI que la funció just a sota s’encarrega de gestionar les peticions que van a:

    • el path /
    • utilitzant una operació get
    Info sobre @decorator

    Aquesta sintaxi @something a Python s’anomena un “decorador”.

    El poses a sobre d’una funció. Com un barret decoratiu maco (suposo que d’aquí ve el terme).

    Un “decorador” pren la funció de sota i fa alguna cosa amb ella.

    En el nostre cas, aquest decorador indica a FastAPI que la funció de sota correspon al path / amb una operació get.

    És el “decorador d’operació de path”

    També pots utilitzar les altres operacions: @app.post(), @app.put(), @app.delete()

    I les més exòtiques: @app.options(), @app.head(), @app.patch(), @app.trace()

    Nota

    Ets lliure d’utilitzar cada operació (mètode HTTP) com vulguis.

    FastAPI no imposa cap significat específic.

    La informació aquí es presenta com una guia, no com un requisit.

    Per exemple, quan utilitzes GraphQL normalment realitzes totes les accions utilitzant només operacions POST.

    Defineix la funció d’operació de path

    Aquesta és la nostra “funció d’operació de path”:

    • path: és /.
    • operació: és get.
    • funció: és la funció sota el “decorador” (sota @app.get("/")).
    main.py
    @app.get("/")
    async def root():
    return {"message": "Hello World"}

    Aquesta és una funció Python.

    Serà cridada per FastAPI cada vegada que rebi una petició a l’URL "/" utilitzant una operació GET.

    En aquest cas, és una funció asíncrona.

    Retorna el contingut

    main.py
    @app.get("/")
    async def root():
    return {"message": "Hello World"}

    Pots retornar un dict, list, valors singulars com str, int, etc.

    També pots retornar models Pydantic (veuràs més sobre això més endavant).

    Hi ha molts altres objectes i models que es convertiran automàticament a JSON (incloent ORMs, etc). Prova d’utilitzar els teus favorits, és molt probable que ja estiguin suportats.

    Paràmetres

    Pots declarar “paràmetres” o “variables” de path amb la mateixa sintaxi utilitzada pels format strings de Python:

    main.py
    from fastapi import FastAPI
    app = FastAPI()
    @app.get("/item/{id}")
    async def read_item(id):
    return {"id": id}

    El valor del paràmetre de path item_id es passarà a la teva funció com l’argument item_id.

    Així, si executes aquest exemple i vas a http://127.0.0.1:8000/item/foo, veuràs una resposta de:

    {"id":"foo"}

    Paràmetres de path amb tipus

    Pots declarar el tipus d’un paràmetre de path a la funció, utilitzant les anotacions de tipus estàndard de Python:

    main.py
    from fastapi import FastAPI
    app = FastAPI()
    @app.get("/item/{id}")
    async def read_item(id: int):
    return {"id": id}

    En aquest cas, id es declara com un int.

    Conversió de dades

    Si executes aquest exemple i obres el teu navegador a http://127.0.0.1:8000/item/3, veuràs una resposta de:

    {"item_id":3}
    Nota

    Fixa’t que el valor que la teva funció ha rebut (i retornat) és 3, com un int de Python, no una string "3".

    Així, amb aquesta declaració de tipus, FastAPI et dóna “parsing” automàtic de peticions.

    Validació de dades

    Però si vas al navegador a http://127.0.0.1:8000/item/foo, veuràs un error HTTP ben explicat:

    {
    "detail": [
    {
    "type": "int_parsing",
    "loc": [
    "path",
    "id"
    ],
    "msg": "Input should be a valid integer, unable to parse string as an integer",
    "input": "foo"
    }
    ]
    }

    perquè el paràmetre de path id tenia un valor de "foo", que no és un int.

    El mateix error apareixeria si proporcionessis un float en lloc d’un int, com a: http://127.0.0.1:8000/item/4.2

    Nota

    Així, amb la mateixa declaració de tipus de Python, FastAPI et dóna validació de dades.

    Fixa’t que l’error també indica exactament el punt on la validació no ha passat.

    Això és increïblement útil mentre desenvolupes i depures codi que interactua amb la teva API.

    Documentació

    I quan obres el teu navegador a http://127.0.0.1:8000/docs, veuràs una documentació automàtica, interactiva de l’API com:

    Fixa’t que el paràmetre id es declara com un enter.

    Pydantic

    Tota la validació de dades es realitza sota el capó per Python - Pydantic, així que obtens tots els beneficis d’això. I saps que estàs en bones mans.

    Pots utilitzar les mateixes declaracions de tipus amb str, float, bool i molts altres tipus de dades complexos.

    L’ordre importa

    En crear operacions de path, pots trobar situacions en què tens un path fix.

    Com /user/me, diguem que és per obtenir dades sobre l’usuari actual.

    I després també pots tenir un path /user/{id} per obtenir dades sobre un usuari específic segons algun ID d’usuari.

    Com que les operacions de path s’avaluen en ordre, necessites assegurar-te que el path per a /user/me es declara abans que el de /user/{id}:

    @app.get("/user/me")
    async def read_user_me():
    return {"user_id": "the current user"}
    @app.get("/user/{id}")
    async def read_user(id: str):
    return {"id": id}

    En cas contrari, el path per a /user/{id} coincidiria també per a /user/me, “pensant” que està rebent un paràmetre id amb un valor de “me”.

    De manera similar, no pots redefinir una operació de path:

    @app.get("/user")
    async def read_users():
    return ["Rick", "Morty"]
    @app.get("/user")
    async def read_users2():
    return ["Bean", "Elfo"]

    La primera sempre s’utilitzarà ja que el path coincideix primer.

    Valors predefinits

    Si tens una operació de path que rep un paràmetre de path, però vols que els possibles valors vàlids del paràmetre de path estiguin predefinits, pots utilitzar un Enum estàndard de Python.

    Importa Enum i crea una subclasse que hereti de str i de Enum.

    Heretant de str, la documentació de l’API podrà saber que els valors han de ser de tipus string i es renderitzaran correctament.

    Després crea atributs de classe amb valors fixos, que seran els valors vàlids disponibles:

    class OrderType(str, Enum):
    buy = "buy"
    sell = "sell"

    Després crea un paràmetre de path amb una anotació de tipus utilitzant la classe enum que has creat (OrderType en aquest cas):

    @app.get("/stock/google/{type}")
    async def stock_get(type: OrderType):
    match type:
    case OrderType.buy:
    return {"google": 101.10}
    case OrderType.sell:
    return {"google": 101.53}

    Com que els valors disponibles per al paràmetre de path estan predefinits, la documentació interactiva els pot mostrar de manera elegant.

    Pots retornar membres enum des de la teva operació de path, fins i tot imbricats en un cos JSON (per exemple, un dict).

    Es convertiran als seus valors corresponents (strings en aquest cas) abans de retornar-los al client:

    @app.get("/stock/google/{type}")
    async def stock_get(type: OrderType):
    match type:
    case OrderType.buy:
    return {"symbol": "google", "price": 101.10, "type": type}
    case OrderType.sell:
    return {"symbol": "google", "price": 101.53, "type": type}

    Paràmetres de consulta

    Quan declares altres paràmetres de funció que no formen part dels paràmetres de path, s’interpreten automàticament com a paràmetres de “consulta”.

    persons = ["Jordi", "Montserrat", "Pere", "Núria", "Marc", "Carme", "Pau", "Mercè", "Josep", "Eulàlia"]
    @app.get("/person")
    async def person_get(skip: int = 0, limit: int = 5):
    return [{"name": person} for person in persons[skip:skip + limit]]

    La consulta és el conjunt de parells clau-valor que van després del ? en un URL, separats per caràcters &.

    Per exemple, a la URL http://127.0.0.1:8000/person?skip=2&limit=3 els paràmetres de consulta són:

    • skip: amb un valor de 2
    • limit: amb un valor de 10

    Com que formen part de l’URL, són “naturalment” strings.

    Però quan els declares amb tipus de Python (a l’exemple anterior, com int), es converteixen a aquest tipus i es validen contra ell.

    Tot el mateix procés que s’aplica als paràmetres de path també s’aplica als paràmetres de consulta: “parsing” de dades, validació de dades i documentació automàtica.

    Valors per defecte

    Com que els paràmetres de consulta no són una part fixa d’un path, poden ser opcionals i poden tenir valors per defecte.

    A l’exemple anterior tenen valors per defecte de skip=0 i limit=5.

    Així, anar a la URL http://127.0.0.1:8000/person seria el mateix que anar a http://127.0.0.1:8000/items/?skip=0&limit=5

    Però si vas a, per exemple http://127.0.0.1:8000/person?skip=8 els valors dels paràmetres a la teva funció seran:

    • skip=8: perquè ho has establert a l’URL
    • limit=5: perquè aquest era el valor per defecte

    Paràmetres opcionals

    De la mateixa manera, pots declarar paràmetres de consulta opcionals, establint el seu valor per defecte a None:

    @app.get("/item/{id}")
    async def item_get(id: str, q: str | None = None):
    if q:
    return {"id": id, "q": q}
    return {"id": id}

    En aquest cas, el paràmetre de funció q serà opcional, i serà None per defecte.

    També observa que FastAPI és prou intel·ligent per adonar-se que el paràmetre de path id és un paràmetre de path i q no ho és, així que és un paràmetre de consulta.

    Conversió de tipus de paràmetre de consulta

    També pots declarar tipus bool, i es convertiran:

    @app.get("/item/{id}")
    async def item_get(id: str, q: str | None = None, short: bool = False):
    item = {"id": id}
    if q:
    item.update({"q": q})
    if not short:
    item.update(
    {"description": "This is an amazing item that has a long description"}
    )
    return item

    En aquest cas, si vas a:

    • http://127.0.0.1:8000/item/foo?short=1
    • http://127.0.0.1:8000/item/foo?short=True
    • http://127.0.0.1:8000/item/foo?short=true
    • http://127.0.0.1:8000/item/foo?short=on
    • http://127.0.0.1:8000/item/foo?short=yes

    o qualsevol altra variació de majúscules i minúscules (majúscules, primera lletra en majúscula, etc.), la teva funció veurà el paràmetre short amb un valor bool de True. En cas contrari com a False.

    Múltiples paràmetres de path i de consulta

    Pots declarar múltiples paràmetres de path i de consulta alhora, FastAPI sap quin és quin.

    I no has de declarar-los en cap ordre específic.

    Es detectaran pel nom:

    @app.get("/user/{user_id}/item/{item_id}")
    async def user_item_get(
    user_id: int, item_id: str, q: str | None = None, short: bool = False
    ):
    item = {"item_id": item_id, "owner_id": user_id}
    if q:
    item.update({"q": q})
    if not short:
    item.update(
    {"description": "This is an amazing item that has a long description"}
    )
    return item

    Paràmetres de consulta obligatoris

    Quan declares un valor per defecte per a paràmetres que no són de path (de moment, només hem vist paràmetres de consulta), llavors no és obligatori.

    Si no vols afegir un valor específic però simplement fer-lo opcional, estableix el valor per defecte com a None.

    Però quan vols fer un paràmetre de consulta obligatori, pots simplement no declarar cap valor per defecte:

    @app.get("/item/{item_id}")
    async def item_get(item_id: str, needy: str):
    item = {"item_id": item_id, "needy": needy}
    return item

    Aquí el paràmetre de consulta needy és un paràmetre de consulta obligatori de tipus str.

    Si obres al teu navegador una URL com http://127.0.0.1:8000/item/foo sense afegir el paràmetre obligatori needy, veuràs un error com:

    { "detail": [
    {
    "type": "missing", "loc": [ "query", "needy"],
    "msg": "Field required",
    "input": null
    }
    ]}

    Com que needy és un paràmetre obligatori, hauries d’establir-lo a la URL http://127.0.0.1:8000/item/foo?needy=sooooneedy això funcionaria:

    {
    "item_id": "foo",
    "needy": "sooooneedy"
    }

    I, per descomptat, pots definir alguns paràmetres com a obligatoris, alguns amb un valor per defecte, i alguns completament opcionals:

    @app.get("/item/{item_id}")
    async def item_get(
    item_id: str, needy: str, skip: int = 0, limit: int | None = None
    ):
    item = {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit}
    return item

    En aquest cas, hi ha 3 paràmetres de consulta:

    • needy, un str obligatori.
    • skip, un int amb un valor per defecte de 0.
    • limit, un int opcional.
    Nota

    També podries utilitzar Enums de la mateixa manera que amb els Paràmetres de Path.

    Request Body

    Quan necessites enviar dades d’un client (diguem, un navegador) a la teva API, les envies com a cos de la petició.

    Un cos de petició són dades enviades pel client a la teva API. Un cos de resposta són les dades que la teva API envia al client.

    La teva API gairebé sempre ha d’enviar un cos de resposta. Però els clients no necessàriament han d’enviar cossos de petició sempre, de vegades només sol·liciten un path, potser amb alguns paràmetres de consulta, però no envien un cos.

    Per declarar un cos de petició, utilitzes models de Python - Pydantic amb tot el seu poder i beneficis.

    Nota

    Per enviar dades, hauries d’utilitzar un de: POST (el més comú), PUT, DELETE o PATCH.

    Enviar un cos amb una petició GET té un comportament indefinit en les especificacions, no obstant això, està suportat per FastAPI, només per a casos d’ús molt complexos/extrems.

    Com que està desaconsellat, la documentació interactiva amb Swagger UI no mostrarà la documentació del cos quan s’utilitza GET, i els proxies al mig poden no suportar-ho.

    Importar BaseModel de Pydantic

    Primer, necessites importar BaseModel de pydantic:

    from pydantic import BaseModel
    class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    @app.post("/item")
    async def item_post(item: Item):
    return item

    Crea el teu model de dades

    Després declares el teu model de dades com una classe que hereta de BaseModel.

    Utilitza tipus estàndard de Python per a tots els atributs:

    class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None

    Igual que quan declares paràmetres de consulta, quan un atribut del model té un valor per defecte, no és obligatori. En cas contrari, és obligatori. Utilitza None per fer-lo només opcional.

    Per exemple, aquest model anterior declara un “objecte” JSON (o dict de Python) com:

    {
    "name": "Foo",
    "description": "An optional description",
    "price": 45.2,
    "tax": 3.5
    }

    …com que description i tax són opcionals (amb un valor per defecte de None), aquest “objecte” JSON també seria vàlid:

    {
    "name": "Foo",
    "price": 45.2
    }

    Declara’l com a paràmetre

    Per afegir-lo a la teva operació de path, declara’l de la mateixa manera que vas declarar els paràmetres de path i de consulta:

    @app.post("/item")
    async def item_post(item: Item):
    return item

    …i declara el seu tipus com el model que has creat, Item.

    Documentació automàtica

    Els esquemes JSON dels teus models formaran part del teu esquema OpenAPI generat, i es mostraran en la documentació interactiva de l’API.

    I també s’utilitzaran en la documentació de l’API dins de cada operació de path que els necessiti.

    Utilitza el model

    Dins de la funció, pots accedir a tots els atributs de l’objecte del model directament:

    @app.post("/item")
    async def item_post(item: Item):
    item_dict = item.model_dump()
    if item.tax is not None:
    price_with_tax = item.price + item.tax
    item_dict.update({"price_with_tax": price_with_tax})
    return item_dict

    Request body + path parameters

    Pots declarar paràmetres de path i cos de petició alhora.

    FastAPI reconeixerà que els paràmetres de la funció que coincideixen amb paràmetres de path s’han de prendre del path, i que els paràmetres de la funció que es declaren com a models Pydantic s’han de prendre del cos de la petició.

    from fastapi import FastAPI
    from pydantic import BaseModel
    class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None
    @app.put("/item/{item_id}")
    async def item_put(item_id: int, item: Item):
    return {"item_id": item_id, **item.dict()}

    Cos de petició + path + paràmetres de consulta

    També pots declarar paràmetres de cos, path i consulta, tots alhora.

    FastAPI reconeixerà cadascun d’ells i prendrà les dades del lloc correcte.

    @app.put("/item/{item_id}")
    async def item_put(item_id: int, item: Item, q: str | None = None):
    result = {"item_id": item_id, **item.dict()}
    if q:
    result.update({"q": q})
    return result

    Els paràmetres de la funció es reconeixeran de la manera següent:

    • Si el paràmetre també es declara al path, s’utilitzarà com a paràmetre de path.
    • Si el paràmetre és d’un tipus singular (com int, float, str, bool, etc.) s’interpretarà com a paràmetre de consulta.
    • Si el paràmetre es declara com del tipus d’un model Pydantic, s’interpretarà com a cos de petició.
    Nota

    FastAPI sabrà que el valor de q no és obligatori pel valor per defecte = None.

    El str | None no l’utilitza FastAPI per determinar que el valor no és obligatori, sabrà que no és obligatori perquè té un valor per defecte de = None.

    Però afegir les anotacions de tipus permetrà que el teu editor et doni un millor suport i detecti errors.

    Activitat

    Crea un API per gestionar un center mèdic.

    A continuació tens la base de dades:

    Doctor

    id: int

    name: str

    surname

    specialty: str

    license_number: str

    Patient

    id: int

    name: str

    surname: str

    birth_date: date

    gender: Gender

    blood_type: BloodType | None = None

    allergies: List[str]

    chronic_conditions: List[str]

    emergency_contact: str

    Visit

    id: int

    visit_date: datetime

    visit_type: VisitType

    vitals: Vital | None

    symptoms: List[str]

    diagnosis: str | None

    notes: str | None

    prescriptions: List[Prescription]

    Prescription

    Has de tenir endpoints per doctor, patient, visit

    Not Found

    Si accedeixes a un dict i al clau que no existeix, és produeix en error 500 Internal Server Error perquè estem accedint al dict amb l’operador [] sense verificar que existeix una entrada per la id que ens han passat com argument.

    El que has de fer és modificar el codi per tornar un error 404 Not Found:

    @app.get("/employee/{id}")
    def employee(id: int):
    if id not in employees.keys():
    raise HTTPException(status_code=404, detail="Employee not found")
    return {"name": employees[id], "job": "clerk"}

    WebSite

    Ves a Render - Dashboard

    Desplega la teva aplicació en el nivell gratuït.

    El Start Command és:

    Terminal window
    fastapi run main.py