2 The API server
Read the tutorial. Build it yourself is the other way to read this part.
This walk goes through rahuldave/cheesecracker on GitHub. This part starts from the code at step-01. The code before this part
To look at this code on your machine, run these commands. The first one makes a clone, if this folder has none:
[ -d cheesecracker ] || git clone https://github.com/rahuldave/cheesecracker
cd cheesecracker
git fetch --quiet --tags --force origin
git switch --detach step-01just setupThe Cheese App talks to its server over HTTP. FastAPI turns Python functions into the answers of HTTP requests, and uvicorn is the server that runs a FastAPI app. We write an app that answers two requests, and run it on this machine.
2.1 FastAPI, and uvicorn to serve it
FastAPI is a library for web APIs. A path is the part of an address after the name of the
server, such as /health. A Python function answers the requests to one path when a line such as @app.get("/health") stands just above it. FastAPI uses the type written next to each argument, such
as float, to check the values of each request. It also writes the pages of the API's docs.
uvicorn is the server. It waits for requests on a numbered port of the machine, such as 9000, and hands each request to the app.
The server needs FastAPI and uvicorn to run, so they are dependencies of the project itself, and not of a group.
This change came from running uv add fastapi uvicorn.
The command adds both packages to dependencies in pyproject.toml, and writes their versions into uv.lock.
uv run uvicorn --versionuvicorn prints its version, and the version of Python that runs it.
What changed
pyproject.toml: FastAPI and uvicorn, the dependencies of the server. 4 lines added, 1 removed. It comes fromuv add fastapi uvicorn. The file- look at
"fastapi>=0.143.2",
- look at
uv.lock: the exact versions of FastAPI, uvicorn, and the packages that they need. 263 lines added. It comes fromuv add fastapi uvicorn. The file
The diff of pyproject.toml
@@ -4,7 +4,10 @@ version = "0.1.0"
description = "The Cheese App's API server, with its tests, its checks and its Docker images"
readme = "README.md"
requires-python = ">=3.12"
-dependencies = []
+dependencies = [
+ "fastapi>=0.143.2",
+ "uvicorn>=0.54.0",
+]
[tool.uv]
# The project is an application, not a library: uv installs its dependencies, and not the project itself2.2 the API server, with / and /health
The app needs a first answer, and a way to tell that it runs. service.py makes the FastAPI app, and answers two
requests. The request / gets a welcome, and /health gets the status of the server.
± See the new file src/api-service/api/service.py
The new file src/api-service/api/service.py makes app, with a title and a version. The CORS middleware lets a web
page from any address call the server. After the comment # Routes, get_index() answers /, and health_check()
answers /health.
uv run --directory src/api-service python -c "from api.service import app; print([r.path for r in app.routes])"Python prints the routes of the app. The last two are / and /health. FastAPI adds the first four, for its
documentation pages.
What changed
src/api-service/api/service.py: the FastAPI app, its CORS middleware, and the routes/and/health. New, 29 lines. The file- look at
return {"status": "healthy"}
- look at
The diff of src/api-service/api/service.py
@@ -0,0 +1,29 @@
+"""
+Cheese App API Service
+"""
+
+from fastapi import FastAPI
+from starlette.middleware.cors import CORSMiddleware
+
+# Setup FastAPI app
+app = FastAPI(title="Cheese API Server", description="API Server for Cheese App", version="v1")
+
+# Enable CORSMiddleware
+app.add_middleware(
+ CORSMiddleware,
+ allow_credentials=False,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+# Routes
+@app.get("/")
+async def get_index():
+ return {"message": "Welcome to the Cheese App CI/CD Tutorial!"}
+
+
+@app.get("/health")
+async def health_check():
+ return {"status": "healthy"}2.3 just serve and just stop, the server on this machine
The app does nothing until a server runs it. The recipe serve starts uvicorn at port 9000, in the background, and
waits until the server answers. The recipe stop stops it. A server is a fixture of the commands that need it. We start
it before them, and stop it after them.
± See the change to .gitignore
The recipe serve runs uvicorn api.service:app with &, so the server runs in the background. --app-dir tells
uvicorn where the package api is, and --reload restarts the server when a file of the code changes. The recipe
keeps the number of the process in .serve.pid, and asks /health once a second until the server answers. stop
stops that process, and waits until the port is free. .gitignore now lists .serve.log and .serve.pid, which git never takes.
just serveThe recipe prints serve: http://localhost:9000, its log in .serve.log. Ask the server for its health.
curl -s localhost:9000/healthcurl sends an HTTP request from the terminal, and prints the answer. macOS and most Linux systems
have it. The server answers {"status":"healthy"}. Open http://localhost:9000/docs in a browser to see the pages that
FastAPI makes for each route.
just stopThe recipe prints stop: stopped. Ask for the health again.
curl -s localhost:9000/healthcurl prints nothing, and fails with the code 7, because no server answers.
What changed
.gitignore: the log of the server and the number of its process. 4 lines added. The file- look at
.serve.pid
- look at
justfile: the recipesserveandstop. 26 lines added. The file- look at
uv run uvicorn api.service:app --app-dir src/api-service --host 0.0.0.0 --port 9000 --reload > .serve.log 2>&1 &
- look at
The diff of .gitignore
@@ -9,6 +9,10 @@ __pycache__/
htmlcov/
.ruff_cache/
+# What just serve writes: the log of the server, and its process id
+.serve.log
+.serve.pid
+
# Secrets: never commit them
secrets/
.envThe diff of justfile
@@ -14,3 +14,29 @@ setup:
# Run the tests. Extra arguments go to pytest: just test tests/unit
test *args:
uv run pytest "$@"
+
+# Start the server on this machine, in the background, at http://localhost:9000. It restarts when a file of the code changes. just stop stops it
+serve:
+ #!/usr/bin/env sh
+ if curl -sf localhost:9000/health > /dev/null; then echo "serve: a server already answers at http://localhost:9000" >&2; exit 1; fi
+ uv run uvicorn api.service:app --app-dir src/api-service --host 0.0.0.0 --port 9000 --reload > .serve.log 2>&1 &
+ echo $! > .serve.pid
+ for i in $(seq 1 30); do
+ if curl -sf localhost:9000/health > /dev/null; then echo "serve: http://localhost:9000, its log in .serve.log"; exit 0; fi
+ sleep 1
+ done
+ echo "serve: no answer after 30 seconds; read .serve.log" >&2
+ exit 1
+
+# Stop the server of just serve
+stop:
+ #!/usr/bin/env sh
+ if [ ! -f .serve.pid ]; then echo "stop: no server of just serve runs here" >&2; exit 1; fi
+ kill "$(cat .serve.pid)" && rm .serve.pid
+ # Wait until the server no longer answers, so that the port is free for the next one
+ for i in $(seq 1 30); do
+ if ! curl -s localhost:9000/health > /dev/null; then echo "stop: stopped"; exit 0; fi
+ sleep 1
+ done
+ echo "stop: the server still answers after 30 seconds" >&2
+ exit 1This part ends at the code at step-02. The code after this part