Skip to content

Commit b046d17

Browse files
authored
Merge pull request #60 from febus982/resource_base_versioning
Resource based API versioning
2 parents bd82cb2 + d56a0cd commit b046d17

File tree

10 files changed

+506
-556
lines changed

10 files changed

+506
-556
lines changed

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
FROM python:3.11-slim as base
1+
ARG PYTHON_VERSION=3.11
2+
FROM python:$PYTHON_VERSION-slim as base
23
ARG UID=2000
34
ARG GID=2000
45
RUN addgroup --gid $GID nonroot && \

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,18 @@ In this way our components are loosely coupled and the application logic
2121
(the domains package) is completely independent of the chosen framework
2222
and the persistence layer.
2323

24-
## HTTP API Docs
24+
## HTTP API Docs and versioning
2525

26-
This application uses [fastapi-versionizer](https://github.com/alexschimpf/fastapi-versionizer)
27-
to provide easy API schema version management.
26+
API documentation is provided by [FastAPI](https://fastapi.tiangolo.com/features/)
27+
on `/docs` and `/redoc` paths using OpenAPI format.
2828

29-
There are 3 different API documentation paths:
29+
I believe that versioning an API at resource level provides a much more
30+
flexible approach than versioning the whole API.
3031

31-
* `/api/v1/docs` and `/api/v1/redoc`: v1 OpenAPI schema
32-
* `/api/v2/docs` and `/api/v2/redoc`: v2 OpenAPI schema
33-
* `/docs` : non-versioned routes OpenAPI schema (e.g. health check endpoint)
32+
The example `books` domain provides 2 endpoints to demonstrate this approach
33+
34+
* `/api/books/v1` (POST)
35+
* `/api/books/v2` (POST)
3436

3537
## Package layers
3638

http_app/__init__.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
from typing import Union
22

33
from fastapi import FastAPI, Request
4-
from fastapi_versionizer import versionize
54
from starlette.responses import JSONResponse
65
from starlette_prometheus import PrometheusMiddleware, metrics
76
from structlog import get_logger
87

98
from config import AppConfig, init_logger
109
from domains import init_domains
11-
from http_app.routes import init_unversioned_routes, init_versioned_routes
10+
from http_app.routes import init_routes
1211
from storage import init_storage
1312

1413

@@ -23,20 +22,9 @@ def create_app(
2322

2423
init_storage()
2524

26-
app.add_middleware(PrometheusMiddleware)
25+
init_routes(app)
2726

28-
init_versioned_routes(app)
29-
versionize(
30-
app=app,
31-
prefix_format="/api/v{major}",
32-
docs_url="/docs",
33-
redoc_url="/redoc",
34-
)
35-
36-
"""
37-
Routes initalised after `versionize` are not versioned
38-
"""
39-
init_unversioned_routes(app)
27+
app.add_middleware(PrometheusMiddleware)
4028
app.add_route("/metrics/", metrics)
4129

4230
return app

http_app/routes/__init__.py

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,10 @@
11
from fastapi import FastAPI
2-
from fastapi.openapi.docs import get_swagger_ui_html
3-
from starlette.responses import HTMLResponse
42

5-
from http_app.routes.api.books import router as api_books_router
6-
from http_app.routes.graphql import graphql_app
7-
from http_app.routes.hello import router as hello_router
8-
from http_app.routes.ping import router as ping_router
3+
from http_app.routes import api, graphql, hello, ping
94

105

11-
def init_versioned_routes(app: FastAPI) -> None:
12-
app.include_router(api_books_router)
13-
14-
15-
def init_unversioned_routes(app: FastAPI) -> None:
16-
app.include_router(ping_router)
17-
app.include_router(hello_router)
18-
app.include_router(graphql_app, prefix="/graphql")
19-
20-
@app.get("/docs", response_class=HTMLResponse, include_in_schema=False)
21-
async def get_api_versions() -> HTMLResponse:
22-
"""
23-
Swagger page for non-versioned routes.
24-
"""
25-
26-
return get_swagger_ui_html( # pragma: no cover
27-
openapi_url=f"{app.openapi_url}",
28-
title=f"{app.title}",
29-
swagger_ui_parameters={"defaultModelsExpandDepth": -1},
30-
)
6+
def init_routes(app: FastAPI) -> None:
7+
app.include_router(api.router)
8+
app.include_router(ping.router)
9+
app.include_router(hello.router)
10+
app.include_router(graphql.router, prefix="/graphql")

http_app/routes/api/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from fastapi import APIRouter
2+
3+
from . import books
4+
5+
router = APIRouter(prefix="/api")
6+
7+
router.include_router(books.router_v1)
8+
router.include_router(books.router_v2)

http_app/routes/api/books.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from fastapi import APIRouter, status
2-
from fastapi_versionizer import api_version
32
from pydantic import BaseModel
43

54
from domains.books import Book, BookData, BookService
65

7-
router = APIRouter(prefix="/books")
6+
router_v1 = APIRouter(prefix="/books/v1")
7+
router_v2 = APIRouter(prefix="/books/v2")
88

99

1010
class CreateBookResponse(BaseModel):
@@ -44,8 +44,7 @@ class Config:
4444
"""
4545

4646

47-
@api_version(1)
48-
@router.post("/", status_code=status.HTTP_201_CREATED)
47+
@router_v1.post("/", status_code=status.HTTP_201_CREATED)
4948
async def create_book(
5049
data: CreateBookRequest,
5150
) -> CreateBookResponse:
@@ -54,9 +53,7 @@ async def create_book(
5453
return CreateBookResponse(book=created_book)
5554

5655

57-
# Example v2 API with added parameter
58-
@api_version(2)
59-
@router.post("/", status_code=status.HTTP_201_CREATED)
56+
@router_v2.post("/", status_code=status.HTTP_201_CREATED)
6057
async def create_book_v2(
6158
data: CreateBookRequest,
6259
some_optional_query_param: bool = False,

http_app/routes/graphql/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55

66
schema = Schema(query=Query)
77

8-
graphql_app: GraphQLRouter = GraphQLRouter(schema)
8+
router: GraphQLRouter = GraphQLRouter(schema)

0 commit comments

Comments
 (0)