Getting started
Using hrefs
with FastAPI
Setting up the application
The hrefs
library resolves URLs automatically. In order for that to work,
it only needs the hrefs.starlette.HrefMiddleware
to be included in the
middleware stack:
from fastapi import FastAPI
from fastapi.middleware import Middleware
from hrefs.starlette import HrefMiddleware
app = FastAPI(middleware=[Middleware(HrefMiddleware)])
Defining a referrable model
from hrefs.starlette import ReferrableModel
class Book(ReferrableModel):
id: int
title: str
class Config:
details_view = "get_book"
@app.get("/books/{id}", response_model=Book)
def get_book(id: int):
book = books.get(id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
return book
To make a model target for hrefs, it needs to:
Inherit from
hrefs.starlette.ReferrableModel
.Have configuration called
details_view
naming the route that will return the details of the referrable model. The URLs will be built by reversed routing, using the primary key of the model as parameters.Have a primary key used as a router parameter in
details_view
. In the above exampleBook.id
is the primary key. This may or may not correspond to the primary key in a database, buthrefs
really isn’t concerned the database layer. By default the primary key is theid
field, but can be configured. See Configuring model key for details.
Note
If the way to set up the route doesn’t look familiar, refer to the FastAPI documentation. The route name defaults to
the name of the handler function, but can also be defined explicitly using
the name
keyword argument in the @app.get()
decorator.
Defining a relationship to the referrable model
from fastapi import HTTPException, Request, Response
from hrefs import Href
from pydantic import BaseModel
class Library(BaseModel):
id: int
books: List[Href[Book]]
@app.get("/libraries/{id}", response_model=Library)
def get_library(id: int):
library = libraries.get(id)
if not library:
raise HTTPException(status_code=404, detail="Library not found")
return library
@app.post("/libraries")
def post_library(library: Library, request: Request):
if any(book.get_key() not in books for book in library.books):
raise HTTPException(
status_code=400, detail="Trying to add nonexisting book to library"
)
libraries[library.id] = library
return Response(
status_code=201,
headers={"Location": request.url_for("get_library", id=library.id)},
)
An annotated type Href[Book]
is used to declare a hyperlink to Book
—
or any other subclass of hrefs.Referrable
for that matter!
The hrefs.Href
class integrates to pydantic. When parsing the books
field , the
following values can automatically be converted to hrefs:
Another
hrefs.Href
instance.An instance of the referred object type (in this case
Book
).A value convertible to the
id
type of the referred object (in this caseint
).A URL that can be matched to the route named in the
details_view
of the referred object type (in this case"get_library"
).
When serializing hrefs.Href
objects to JSON (FastAPI serializes the
object returned from the route handler behind the scenes!), it will be
represented by URL.
A full working example
The tests/
folder contains a minimal toy application demonstrating how the
hrefs
library is used. The code is reproduced here for convenience:
"""A test application
This is a self-contained demo of the `hrefs` library. You can run it with your
favorite ASGI server, and interact with it using your favorite HTTP client.
Its state is only contained in memory, so it is advisable also to manage your
libraries in a more persistent format!
"""
# pylint: disable=wrong-import-position,wrong-import-order
from typing import Dict, List
import uuid
from fastapi import FastAPI, HTTPException, Response
from fastapi.middleware import Middleware
from pydantic import BaseModel, parse_obj_as
from typing_extensions import Annotated
from hrefs import Href, PrimaryKey
from hrefs.starlette import ReferrableModel, HrefMiddleware
class _BookBase(BaseModel):
title: str
class BookCreate(_BookBase):
"""Book creation payload"""
class Book(ReferrableModel, _BookBase):
"""Book in a library
A book has a `title` and identity, represented by a hyperlink in the `self`
field.
"""
self: Annotated[Href["Book"], PrimaryKey(type_=uuid.UUID, name="id")]
class Config:
"""Book config
The `hrefs` library requires `details_view` option, which is the name of
the Starlette route (in this case FastAPI route that is a Starlette
route under the hood) serving the representation of a book.
"""
details_view = "get_book"
Book.update_forward_refs()
class _LibraryBase(BaseModel):
books: List[Href[Book]]
class LibraryCreate(_LibraryBase):
"""Library creation payload"""
class Library(ReferrableModel, _LibraryBase):
"""A library containing some books
A library has a list of `books`, represented by hyperlinks. Its own identity
in `self` is also a hyperlink.
"""
self: Annotated[Href["Library"], PrimaryKey(type_=uuid.UUID, name="id")]
class Config:
"""Book config
The `hrefs` library requires `details_view` option, which is the name of
the Starlette route (in this case FastAPI route that is a Starlette
route under the hood) serving the representation of a library.
"""
details_view = "get_library"
Library.update_forward_refs()
books: Dict[Href[Book], Book] = {}
libraries: Dict[Href[Library], Library] = {}
app = FastAPI(middleware=[Middleware(HrefMiddleware)])
@app.get("/libraries/{id}", response_model=Library)
def get_library(id: uuid.UUID):
"""Retrieve a library identified by `id`"""
href = parse_obj_as(Href[Library], id)
library = libraries.get(href)
if not library:
raise HTTPException(status_code=404, detail="Library not found")
return library
@app.post("/libraries")
def post_library(library: LibraryCreate):
"""Create a new library
A library can contain any number of books, represented by a list of
hyperlinks. The books can either be referred by their `id` (an UUID encoded
as a string), or a hyperlink (an URL previously returned by `POST /books`
call).
The identity will be automatically generated, and returned in the `Location`
header.
"""
if any(book not in books for book in library.books):
raise HTTPException(
status_code=400, detail="Trying to add nonexisting book to library"
)
new_library = Library(self=uuid.uuid4(), **library.dict())
libraries[new_library.self] = new_library
return Response(
status_code=201,
headers={"Location": new_library.self.url},
)
@app.get("/books/{id}", response_model=Book)
def get_book(id: uuid.UUID):
"""Retrieve a book identified by `id`"""
href = parse_obj_as(Href[Book], id)
book = books.get(href)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
return book
@app.post("/books", status_code=204)
def post_book(book: BookCreate):
"""Create a new book
The identity will be automatically generated, and returned in the `Location`
header."""
new_book = Book(self=uuid.uuid4(), **book.dict())
books[new_book.self] = new_book
return Response(status_code=201, headers={"Location": new_book.self.url})
You can run the test application in your favorite ASGI server:
$ uvicorn tests.app:app
INFO: Started server process
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Then use your favorite HTTP client and start creating libraries. You can use
either IDs or URL to refer to the books in the second POST
request — the
library knows how to parse either!
$ http POST localhost:8000/books title='My first book'
HTTP/1.1 201 Created
Transfer-Encoding: chunked
location: http://localhost:8000/books/fb69a5d5-b956-4189-8e3e-89f001815bec
server: uvicorn
$ http POST localhost:8000/libraries books:='["http://localhost:8000/books/fb69a5d5-b956-4189-8e3e-89f001815bec"]'
HTTP/1.1 201 Created
Transfer-Encoding: chunked
location: http://localhost:8000/libraries/50c224ff-c9f4-4186-8a05-4999f522ea67
server: uvicorn
$ http GET http://localhost:8000/libraries/50c224ff-c9f4-4186-8a05-4999f522ea67
HTTP/1.1 200 OK
content-length: 50
content-type: application/json
server: uvicorn
{
"books": [
"http://localhost:8000/books/fb69a5d5-b956-4189-8e3e-89f001815bec"
],
"self": "http://localhost:8000/libraries/50c224ff-c9f4-4186-8a05-4999f522ea67"
}
Using hrefs
with Starlette
While the library was written with FastAPI in mind, the integration doesn’t actually depend on FastAPI, only pydantic and Starlette. You can perfectly well write Starlette apps containing hrefs. You just need to ensure that:
For each
hrefs.starlette.ReferrableModel
there is a named route matching thedetails_view
configuration.The
hrefs.starlette.HrefMiddleware
is added as middleware to the application.In the responses, the pydantic models containing references are explicitly serialized using
model.json()
method.
Writing a custom integration
The hrefs.Href
class can refer to any type implementing the
hrefs.Referrable
protocol. You can also use the
hrefs.BaseReferrableModel
ABC to get part of the implementation for
free.