2 The server under test

Build it yourself. Read the tutorial 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

If you start at this part, make your clone as Get the project says, and then run this command. It puts in your clone the files that this part starts from. A file that the project deleted before this part stays, so delete it if git check lists it:

git fetch --quiet --tags --force origin; git rm -rq --cached --ignore-unmatch -- :/ && git restore --source step-01 --staged --worktree -- :/

The integration tests and the system tests need a server to call. This section writes it, with two answers, / and /health, and the recipes just serve and just stop.

just setup

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

uv add fastapi uvicorn

The command adds both packages to dependencies in pyproject.toml, and writes their versions into uv.lock.

uv run uvicorn --version

uvicorn prints its version, and the version of Python that runs it.

Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:

git check 18dd9dfb1c95

If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:

git fetch --quiet --tags --force origin; git restore --source step-01 --staged -- :/ && git restore --source 18dd9dfb1c95 --staged --worktree -- :/

What changed

  • pyproject.toml: FastAPI and uvicorn, the dependencies of the server. 4 lines added, 1 removed. It comes from uv add fastapi uvicorn. The file
    • look at "fastapi>=0.143.2",
  • uv.lock: the exact versions of FastAPI, uvicorn, and the packages that they need. 263 lines added. It comes from uv add fastapi uvicorn. The file

The diff that uv add fastapi uvicorn makes in 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 itself

This change on GitHub The code after it

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

Apply the patch to src/api-service/api/service.py, from the top of the project:

curl -fsSL https://github.com/rahuldave/cheesecracker/commit/acee14f7dff43d6c5b76234b3a016985d74c60d2.diff | git apply --include=src/api-service/api/service.py

Apply the patch, and read the file from the top. It makes app, and adds the CORS middleware, which lets a web page from any address call the server. Then it defines the two routes after the comment # Routes.

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.

Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:

git check acee14f7dff4

If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:

git restore --source 18dd9dfb1c95 --staged -- :/ && git restore --source acee14f7dff4 --staged --worktree -- :/

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"}

The patch to 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"}

This change on GitHub The code after it

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.

Apply the patch to justfile, from the top of the project:

curl -fsSL https://github.com/rahuldave/cheesecracker/commit/69c3bf65c78cf8e96bf8f9ea1dde29279db1351a.diff | git apply --include=justfile

Apply the patch to .gitignore, from the top of the project:

curl -fsSL https://github.com/rahuldave/cheesecracker/commit/69c3bf65c78cf8e96bf8f9ea1dde29279db1351a.diff | git apply --include=.gitignore

Read the recipes serve and stop at the end of the justfile. Then read the two new lines of .gitignore.

just serve

The recipe prints serve: http://localhost:9000, its log in .serve.log. Ask the server for its health.

curl -s localhost:9000/health

curl 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 stop

The recipe prints stop: stopped. Ask for the health again.

curl -s localhost:9000/health

curl prints nothing, and fails with the code 7, because no server answers.

Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:

git check 69c3bf65c78c

If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:

git fetch --quiet --tags --force origin; git restore --source acee14f7dff4 --staged -- :/ && git restore --source step-02 --staged --worktree -- :/

What changed

  • .gitignore: the log of the server and the number of its process. 4 lines added. The file
    • look at .serve.pid
  • justfile: the recipes serve and stop. 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 &

The patch to .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/
 .env

The patch to 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 1

This change on GitHub

This part ends at the code at step-02. The code after this part