{ "cells": [ { "cell_type": "markdown", "id": "db76f253-0bfd-47be-b145-8c6c6d250472", "metadata": {}, "source": [ "# Backend: FastAPI\n", "\n", "An API (Application Programming Interface) is just a small program that answers requests. You send it a question like “give me item 42,” it sends back an answer in JSON. \n", "\n", "Think of it as a helpful librarian for your data.\n", "\n", "**Note on JSON**: JSON (JavaScript Object Notation) is a lightweight, text-based format for structuring data as key–value pairs and arrays that’s easy for humans to read and for machines to parse.\n", "\n", "\n", "[FastAPI](https://github.com/fastapi/fastapi) publishes a **menu** of everything your API serves (paths, inputs, outputs).\n", "\n", "\n", "Generally, you would open `http://127.0.0.1:8000/docs` and you get buttons you can click to try the API. That “menu” (called **OpenAPI**) is also usable by other tools to interact with your app.\n", "\n", "\n", "In **full stack**, FastAPI is typically the **backend**: it runs on the server, exposes HTTP APIs, handles business logic, talks to databases/queues, and is served by an ASGI server like **Uvicorn**; while the frontend (e.g., Next.js/[React](https://en.wikipedia.org/wiki/React_(software))) calls those FastAPI endpoints over HTTP.\n", "\n", "\n", "**ASGI** (Asynchronous Server Gateway Interface) is the Python standard that connects **web servers** to **async-capable apps** (like FastAPI), enabling concurrency and long-lived connections (i.e., can handle multiple requests in overlapping time). " ] }, { "cell_type": "code", "execution_count": 1, "id": "78c624c4-bb7f-4b84-bbeb-fbe18d8a98b4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Collecting fastapi>=0.115\n", " Downloading fastapi-0.121.0-py3-none-any.whl.metadata (28 kB)\n", "Collecting uvicorn>=0.30 (from uvicorn[standard]>=0.30)\n", " Using cached uvicorn-0.38.0-py3-none-any.whl.metadata (6.8 kB)\n", "Collecting starlette<0.50.0,>=0.40.0 (from fastapi>=0.115)\n", " Downloading starlette-0.49.3-py3-none-any.whl.metadata (6.4 kB)\n", "Collecting pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4 (from fastapi>=0.115)\n", " Using cached pydantic-2.12.3-py3-none-any.whl.metadata (87 kB)\n", "Requirement already satisfied: typing-extensions>=4.8.0 in /Users/boris/MPhil/ResearchComputing/venvs/c1_base_env/lib/python3.12/site-packages (from fastapi>=0.115) (4.15.0)\n", "Collecting annotated-doc>=0.0.2 (from fastapi>=0.115)\n", " Using cached annotated_doc-0.0.3-py3-none-any.whl.metadata (6.6 kB)\n", "Collecting click>=7.0 (from uvicorn>=0.30->uvicorn[standard]>=0.30)\n", " Using cached click-8.3.0-py3-none-any.whl.metadata (2.6 kB)\n", "Requirement already satisfied: h11>=0.8 in /Users/boris/MPhil/ResearchComputing/venvs/c1_base_env/lib/python3.12/site-packages (from uvicorn>=0.30->uvicorn[standard]>=0.30) (0.16.0)\n", "Collecting httptools>=0.6.3 (from uvicorn[standard]>=0.30)\n", " Using cached httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl.metadata (3.5 kB)\n", "Collecting python-dotenv>=0.13 (from uvicorn[standard]>=0.30)\n", " Downloading python_dotenv-1.2.1-py3-none-any.whl.metadata (25 kB)\n", "Requirement already satisfied: pyyaml>=5.1 in /Users/boris/MPhil/ResearchComputing/venvs/c1_base_env/lib/python3.12/site-packages (from uvicorn[standard]>=0.30) (6.0.3)\n", "Collecting uvloop>=0.15.1 (from uvicorn[standard]>=0.30)\n", " Using cached uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl.metadata (4.9 kB)\n", "Collecting watchfiles>=0.13 (from uvicorn[standard]>=0.30)\n", " Using cached watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl.metadata (4.9 kB)\n", "Requirement already satisfied: websockets>=10.4 in /Users/boris/MPhil/ResearchComputing/venvs/c1_base_env/lib/python3.12/site-packages (from uvicorn[standard]>=0.30) (15.0.1)\n", "Collecting annotated-types>=0.6.0 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi>=0.115)\n", " Using cached annotated_types-0.7.0-py3-none-any.whl.metadata (15 kB)\n", "Collecting pydantic-core==2.41.4 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi>=0.115)\n", " Using cached pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl.metadata (7.3 kB)\n", "Collecting typing-inspection>=0.4.2 (from pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4->fastapi>=0.115)\n", " Using cached typing_inspection-0.4.2-py3-none-any.whl.metadata (2.6 kB)\n", "Requirement already satisfied: anyio<5,>=3.6.2 in /Users/boris/MPhil/ResearchComputing/venvs/c1_base_env/lib/python3.12/site-packages (from starlette<0.50.0,>=0.40.0->fastapi>=0.115) (4.11.0)\n", "Requirement already satisfied: idna>=2.8 in /Users/boris/MPhil/ResearchComputing/venvs/c1_base_env/lib/python3.12/site-packages (from anyio<5,>=3.6.2->starlette<0.50.0,>=0.40.0->fastapi>=0.115) (3.10)\n", "Requirement already satisfied: sniffio>=1.1 in /Users/boris/MPhil/ResearchComputing/venvs/c1_base_env/lib/python3.12/site-packages (from anyio<5,>=3.6.2->starlette<0.50.0,>=0.40.0->fastapi>=0.115) (1.3.1)\n", "Downloading fastapi-0.121.0-py3-none-any.whl (109 kB)\n", "Using cached uvicorn-0.38.0-py3-none-any.whl (68 kB)\n", "Using cached annotated_doc-0.0.3-py3-none-any.whl (5.5 kB)\n", "Using cached click-8.3.0-py3-none-any.whl (107 kB)\n", "Using cached httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl (110 kB)\n", "Using cached pydantic-2.12.3-py3-none-any.whl (462 kB)\n", "Using cached pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl (1.9 MB)\n", "Downloading python_dotenv-1.2.1-py3-none-any.whl (21 kB)\n", "Downloading starlette-0.49.3-py3-none-any.whl (74 kB)\n", "Using cached uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl (1.4 MB)\n", "Using cached watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl (391 kB)\n", "Using cached annotated_types-0.7.0-py3-none-any.whl (13 kB)\n", "Using cached typing_inspection-0.4.2-py3-none-any.whl (14 kB)\n", "Installing collected packages: uvloop, typing-inspection, python-dotenv, pydantic-core, httptools, click, annotated-types, annotated-doc, watchfiles, uvicorn, starlette, pydantic, fastapi\n", "Successfully installed annotated-doc-0.0.3 annotated-types-0.7.0 click-8.3.0 fastapi-0.121.0 httptools-0.7.1 pydantic-2.12.3 pydantic-core-2.41.4 python-dotenv-1.2.1 starlette-0.49.3 typing-inspection-0.4.2 uvicorn-0.38.0 uvloop-0.22.1 watchfiles-1.1.1\n", "\n", "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m25.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m25.3\u001b[0m\n", "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n" ] } ], "source": [ "!pip install \"fastapi>=0.115\" \"uvicorn[standard]>=0.30\"" ] }, { "cell_type": "markdown", "id": "62bd779d-9314-4d35-9e70-244b7d59e10e", "metadata": {}, "source": [ "You can put these dependencies inside a requirements file: \n", "\n", "```bash\n", "#requirements.text\n", "fastapi>=0.115\n", "uvicorn[standard]>=0.30\n", "numpy\n", "scipy\n", "matplotlib\n", "```\n", "\n", "And install with:\n", "\n", "```bash\n", "pip install -r requirements.txt\n", "```\n" ] }, { "cell_type": "markdown", "id": "0f1b7b02-53dd-401b-a205-bf797102e915", "metadata": {}, "source": [ "To test a simple example, let us create a `main.py` file.\n", "\n", "```python\n", "# main.py\n", "from fastapi import FastAPI\n", "from pydantic import BaseModel\n", "\n", "app = FastAPI()\n", "\n", "class Item(BaseModel):\n", " name: str\n", " price: float\n", "\n", "@app.get(\"/\")\n", "def hello():\n", " return {\"message\": \"Hello 👋\"}\n", "\n", "@app.post(\"/items\")\n", "def create(item: Item):\n", " return {\"ok\": True, \"item\": item}\n", "```" ] }, { "cell_type": "markdown", "id": "b96188a8-5b35-474b-959d-e5b71fbb3e2b", "metadata": {}, "source": [ "You can then run the app with:\n", "\n", "```bash\n", "uvicorn main:app --reload\n", "```\n", "\n", "A common error that main appear is: \n", "```\n", "ERROR: [Errno 48] Address already in use\n", "```\n", "\n", "You can see which ports are busy with \n", "\n", "```\n", "lsof -iTCP -sTCP:LISTEN -n -P\n", "```\n", "\n", "Or if a specific port is busy with:\n", "```\n", "lsof -i :8000\n", "```\n", "\n", "Then visit [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs).\n", "\n", "click `POST /items`, try sending `{\"name\": 123, \"price\": \"oops\"}` and see it politely refuse with a helpful error.\n", "\n", "Then send `{\"name\":\"apple\",\"price\":1.2}` and it works." ] }, { "cell_type": "markdown", "id": "682beeb4-66ce-4179-8eaf-06d637e337cf", "metadata": {}, "source": [ "You can also `curl` to your app:" ] }, { "cell_type": "code", "execution_count": 6, "id": "bcca782e-a241-4aa3-a29f-8844e6bdb31b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\"ok\":true,\"item\":{\"name\":\"apple\",\"price\":1.2}}" ] } ], "source": [ "!curl -X 'POST' \\\n", " 'http://127.0.0.1:8000/items' \\\n", " -H 'accept: application/json' \\\n", " -H 'Content-Type: application/json' \\\n", " -d '{\"name\":\"apple\",\"price\":1.2}'" ] }, { "cell_type": "code", "execution_count": 4, "id": "a470292e-cb4e-4ce5-984b-010927b48dd3", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\"detail\":[{\"type\":\"float_parsing\",\"loc\":[\"body\",\"price\"],\"msg\":\"Input should be a valid number, unable to parse string as a number\",\"input\":\"p\"}]}" ] } ], "source": [ "!curl -X 'POST' \\\n", " 'http://127.0.0.1:8000/items' \\\n", " -H 'accept: application/json' \\\n", " -H 'Content-Type: application/json' \\\n", " -d '{\"name\":\"apple\",\"price\":\"p\"}'" ] }, { "cell_type": "code", "execution_count": 4, "id": "4804d726-bd15-45b9-92ec-b18cea5e0296", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{\"message\":\"Hello 👋\"}" ] } ], "source": [ "!curl http://127.0.0.1:8000/" ] }, { "cell_type": "markdown", "id": "a4c64ed1-6da2-4b8c-a0d0-8e1bbd0963c5", "metadata": {}, "source": [ "In these `curl` commands the `-X` option is setting teh HTTP method. You usually don’t need `-X` for `GET` as curl defaults to GET if no body is sent." ] }, { "cell_type": "markdown", "id": "9de94395-7b16-4acc-9fc3-216efe0b6dbc", "metadata": {}, "source": [ "Let us take a closer look at the program.\n", "\n", "### Declaration\n", "\n", "The first two lines simply import the dependencies:\n", "\n", "\n", "- `FastAPI` is the web framework class you’ll instantiate.\n", "\n", "- `BaseModel` (from [Pydantic](https://docs.pydantic.dev/latest/)) is used to define data shapes with type hints and validation.\n", "\n", "\n", "Then,\n", "\n", "```python\n", "app = FastAPI()\n", "```\n", "\n", "creates the app. `app` is the ASGI application, run/served by Uvicorn.\n", "\n", "Then, \n", "\n", "### Shape of data\n", "\n", "```python\n", "class Item(BaseModel):\n", " name: str\n", " price: float\n", "```\n", "\n", "Defines the shape of data your API expects for an `Item`. `BaseModel` makes it a Pydantic model:\n", " - Validates types (e.g., name must be text, price must be a number).\n", " - Converts compatible types (e.g., \"1.2\" → 1.2) and raises a 422 Unprocessable Entity if invalid.\n", "\n", "\n", "### Endpoint\n", "\n", "`@app.get(\"/\")` declares a GET endpoint at the root path `/`\n", "\n", "```python\n", "@app.get(\"/\")\n", "def hello():\n", " return {\"message\": \"Hello 👋\"}\n", "```\n", "\n", "When a GET request hits /, FastAPI calls hello() and serializes the returned Python dict to JSON:\n", "\n", "- Response body: {\"message\": \"Hello 👋\"}\n", "- Default status code: `200 OK`\n", "\n", "\n", "`@app.post(\"/items\")` declares a POST endpoint at /items.\n", "\n", "```python\n", "@app.post(\"/items\")\n", "def create(item: Item):\n", " return {\"ok\": True, \"item\": item}\n", "```\n", "\n", "The parameter `item: Item` tells FastAPI:\n", "\n", "*Read the request body as JSON, parse & validate it against the Item model.*\n", "\n", "If the body is missing/invalid → return 422 with error details.\n", "\n", "If valid, you get a Python Item instance inside the function.\n", "\n", "Returning {\"ok\": True, \"item\": item}:\n", "\n", "FastAPI sees item is a Pydantic model, converts it to JSON automatically.\n", "\n", "Default status code is `200 OK`\n", "\n", "\n", "### Port\n", "\n", "\n", "* **`http://`** — the protocol (how we talk). Here it is plain [HTTP](https://en.wikipedia.org/wiki/HTTP).\n", " \n", "* **`127.0.0.1`** — the **loopback** address, aka **localhost**. It always means “this computer”. Only apps on the same machine can reach it.\n", "\n", "\n", "* **`:8000`** — the **port** number (like a door on the house). Uvicorn’s dev server defaults to 8000 unless you change it.\n", "\n", " \n", "* (No extra path) **`/`** — the **root** of your API. The “root path” / is a URL path, **not a filesystem path**. It just means “the endpoint at the top of the API” (e.g., GET /).\n", "\n", "So `http://127.0.0.1:8000` = “Use HTTP to talk to the API running on *my* machine, on port 8000, at the root path.”\n", "\n", "### Handy extras\n", "\n", "* You can also use: `http://localhost:8000/`\n", "* The docs live at: `http://127.0.0.1:8000/docs` \n", "* Change port/host when running Uvicorn:\n", "\n", " ```bash\n", " # different port\n", " uvicorn main:app --reload --port 3000\n", "\n", " # allow other devices on your network to access it (dev only!)\n", " uvicorn main:app --host 127.0.0.1 --port 8000\n", " ```\n", "\n", "* Print just body with `curl`\n", "\n", "```bash\n", "curl -s http://127.0.0.1:8000/ | jq\n", "```\n", "\n", "The `-s` option silences the progress bars.\n", "\n", "\n", "## Adding Endpoint\n", "\n", "\n", "In this example we add an FFT endpoint\n", "\n", "```python\n", "@app.get(\"/fft\")\n", "def fft_endpoint(n: int = 4096, L: float = 2*np.pi, k: int = 0):\n", " \"\"\"\n", " Compute FFT of sin(5x)*cos(9x) on [0,L) with N samples and return the value at harmonic k.\n", " - n: number of samples\n", " - L: domain length\n", " - k: harmonic index (e.g., 0, ±4, ±14). Maps to FFT bin m ≈ k*L/(2π).\n", " \"\"\"\n", " try:\n", " return fft_at_k(k=k, n=n, L=L)\n", " except ValueError as e:\n", " raise HTTPException(status_code=400, detail=str(e))\n", "```\n", "\n", "where `fft_at_k` is defined somewhere else (maybe in the same file, or in another package).\n", "\n", "\n", "### Endpoint types\n", "\n", "Here `/fft` endpoint is a pure read-only calculation: given inputs (k, n, L) it returns a result and doesn’t create/modify/delete server state. \n", "It is therefore a `GET` request. \n", "\n", "\n", "Other endpoints include `POST`, `PUT`, `DELETE`. Here is a brief summary of what these do:\n", "\n", "\n", "* **GET** — *Read-only, safe, idempotent, cacheable.*\n", " Use for fetching or computing without changing server state.\n", "\n", " ```bash\n", " curl -s -X GET \"http://127.0.0.1:8000/fft?k=14&n=4096&L=6.283185307179586\"\n", " ```\n", "\n", " Typical codes: `200 OK`, `304 Not Modified`, `404 Not Found`.\n", "\n", "* **POST** — *Create or submit; not idempotent.*\n", " Use for creating resources, submitting forms/jobs, or sending complex inputs in the body.\n", "\n", " ```bash\n", " curl -s -X POST http://127.0.0.1:8000/items \\\n", " -H \"Content-Type: application/json\" \\\n", " -d '{\"name\":\"notebook\",\"price\":12.5}'\n", " ```\n", "\n", " Typical codes: `201 Created`, `202 Accepted`, `400/422` validation errors.\n", "\n", "* **PUT** — *Replace (or create at known URL); idempotent.*\n", " Use to fully replace a resource. Same request repeated → same result.\n", "\n", " ```bash\n", " curl -s -X PUT http://127.0.0.1:8000/items/42 \\\n", " -H \"Content-Type: application/json\" \\\n", " -d '{\"name\":\"notebook\",\"price\":13.0}'\n", " ```\n", "\n", " Typical codes: `200 OK`, `201 Created`, `204 No Content`.\n", "\n", "* **PATCH** — *Partial update; not necessarily idempotent (often treated as such).*\n", " Use to modify part of a resource.\n", "\n", " ```bash\n", " curl -s -X PATCH http://127.0.0.1:8000/items/42 \\\n", " -H \"Content-Type: application/json\" \\\n", " -d '{\"price\":13.5}'\n", " ```\n", "\n", " Typical codes: `200 OK`, `204 No Content`.\n", "\n", "* **DELETE** — *Remove; idempotent by convention.*\n", " Repeating a successful DELETE keeps it gone.\n", "\n", " ```bash\n", " curl -s -X DELETE http://127.0.0.1:8000/items/42\n", " ```\n", "\n", " Typical codes: `200 OK`, `202 Accepted`, `204 No Content`, `404 Not Found`.\n", "\n", "* **HEAD** — *Headers only (like GET without body).* Useful for checks.\n", "\n", " ```bash\n", " curl -I http://127.0.0.1:8000/fft?k=14\n", " ```\n", "\n", "* **OPTIONS** — *What methods are allowed / CORS preflight.*\n", "\n", " ```bash\n", " curl -s -X OPTIONS http://127.0.0.1:8000/fft -i\n", " ```\n", "\n", "**Rules of thumb**\n", "\n", "* Read-only? → **GET**.\n", "* Create/submit/long payloads? → **POST**.\n", "* Full replace at known URL? → **PUT**.\n", "* Partial update? → **PATCH**.\n", "* Delete? → **DELETE**.\n", "\n", "(And for APIs: document expected status codes and request/response schemas.)\n", "\n", "In this context, **Idempotent** means that doing the same request multiple times has the same effect as doing it once.\n", "\n", "\n", "## Status codes\n", "\n", "\n", "Sending HTTP requests usually ends with status codes being received. \n", "\n", "**1xx — Informational**\n", "\n", "* **100 Continue** – Client may send the body.\n", "* **101 Switching Protocols** – Upgrading (e.g., to WebSocket).\n", "* **102 Processing** – Server has accepted but not finished (WebDAV).\n", "\n", "**2xx — Success**\n", "\n", "* **200 OK** – Standard successful response (GET/PUT/PATCH/DELETE).\n", "* **201 Created** – New resource created (usually with `Location` header).\n", "* **202 Accepted** – Accepted for async processing (job queued).\n", "* **204 No Content** – Success, no response body (e.g., DELETE or PUT with no body).\n", "* **206 Partial Content** – Ranged responses.\n", "\n", "**3xx — Redirection**\n", "\n", "* **301 Moved Permanently** – Resource has a new URL.\n", "* **302 Found** – Temporary redirect (common but legacy semantics).\n", "* **303 See Other** – Redirect for POST/redirect/GET pattern.\n", "* **304 Not Modified** – Use cached copy (conditional GET).\n", "* **307 Temporary Redirect** – Redirect, **preserve method**.\n", "* **308 Permanent Redirect** – Permanent, **preserve method**.\n", "\n", "**4xx — Client Errors**\n", "\n", "* **400 Bad Request** – Malformed syntax/params.\n", "* **401 Unauthorized** – Missing/invalid auth (use with `WWW-Authenticate`).\n", "* **403 Forbidden** – Authenticated but not allowed.\n", "* **404 Not Found** – No such resource/route.\n", "* **405 Method Not Allowed** – Wrong HTTP method for this route.\n", "* **406 Not Acceptable** – Content negotiation failed (e.g., `Accept` header).\n", "* **408 Request Timeout** – Client took too long to send.\n", "* **409 Conflict** – Version/edit conflict, duplicate, business rule clash.\n", "* **410 Gone** – Resource intentionally removed.\n", "* **412 Precondition Failed** – ETag/time precondition failed.\n", "* **413 Payload Too Large** – Body too big.\n", "* **415 Unsupported Media Type** – Wrong `Content-Type`.\n", "* **418 I’m a teapot** – Easter egg (don’t use in prod 😄).\n", "* **422 Unprocessable Entity** – Semantically invalid (FastAPI/Pydantic validation).\n", "* **429 Too Many Requests** – Rate limit hit (include `Retry-After`).\n", "\n", "**5xx — Server Errors**\n", "\n", "* **500 Internal Server Error** – Generic server crash/bug.\n", "* **501 Not Implemented** – Method/feature not supported.\n", "* **502 Bad Gateway** – Upstream error (proxy/gateway).\n", "* **503 Service Unavailable** – Temporarily overloaded/maintenance (use `Retry-After`).\n", "* **504 Gateway Timeout** – Upstream didn’t respond in time.\n", "\n", "When to use what:\n", "\n", "* **GET** success: `200` (or `206` if partial), `304` if cached.\n", "* **POST (create)**: `201` + `Location: /items/{id}`.\n", "* **POST (enqueue job)**: `202` + job status URL.\n", "* **PUT/PATCH (update)**: `200` with body or `204` without body.\n", "* **DELETE**: `204` (or `200` with a body), `404` if not found.\n", "* **Validation failure**: `422` (FastAPI default).\n", "* **Auth**: `401` (no/invalid creds), `403` (not allowed).\n", "* **Conflict**: `409` (duplicate, version mismatch).\n", "\n", "\n", "\n", "You don’t need to define every status code. FastAPI/Uvicorn handle many for you. You only set codes when you want something different from the defaults.\n", "\n", "What FastAPI handles automatically:\n", "\n", " * 200 OK on success if you just return ... (your /fft and /items do this).\n", "\n", " * 404 Not Found when the route doesn’t exist (e.g., POST /items/42 if not defined).\n", "\n", " * 405 Method Not Allowed when the path exists but the HTTP method doesn’t.\n", "\n", " * 422 Unprocessable Entity when request data fails validation (e.g., price:\"p\").\n", "\n", " * 500 Internal Server Error for uncaught exceptions.\n", "\n", "\n", "## Interactive API docs page\n", "\n", "\n", "Visit [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs). This is an interactive API docs page that FastAPI serves automatically.\n", "\n", "It’s powered by Swagger UI and reads your app’s OpenAPI schema to render endpoints, parameters, models, and example responses.\n", "\n", "You can “Try it out”: send real requests from the browser, see status codes, headers, and JSON.\n", "\n", "\n", "\n", "OpenAPI is the specification/standard for describing HTTP APIs (paths, methods, params, schemas, errors). It used to be called Swagger. \n", "\n", "You can customize the doc header, for example:\n", "\n", "```python\n", "app = FastAPI(\n", " title=\"Signal Lab API\",\n", " description=\"Endpoints for FFT demos (sin-cos combos) and simple items CRUD.\",\n", " version=\"0.1.0\",\n", " contact={\"name\": \"Boris Bolliet\"},\n", " license_info={\"name\": \"MIT\"},\n", ")\n", "```\n", "\n", "\n", "## Note on REST\n", "\n", "There is a link between [REST](https://en.wikipedia.org/wiki/REST) and FastAPI. \n", "\n", "REST is an architectural style (resources + HTTP verbs + status codes + statelessness).\n", "\n", "FastAPI is a web framework that gives you the tools to implement that style cleanly:\n", "\n", "* Decorators for HTTP methods: @app.get/post/put/patch/delete.\n", "\n", "* Path params & query params → map naturally to resource identifiers and filters.\n", "\n", "* Pydantic models enforce request/response schemas (great for REST contracts).\n", "\n", "* Status codes & headers are simple to set (status_code=201, Location, etc.).\n", "\n", "* OpenAPI docs auto-generated so your REST contract is visible at /docs.\n", "\n", "\n", "FastAPI encourages REST best practices but doesn’t force them—.\n", "\n", "If you follow REST conventions (nouns for paths, proper verbs, status codes, headers), FastAPI gives you type-safe validation and live docs “for free.”" ] }, { "cell_type": "markdown", "id": "582182d9-85ad-4325-9aee-1c9a07a1db13", "metadata": {}, "source": [ "# Frontend: Next js\n", "\n", "## Structure and Initialization\n", "\n", "\n", "Broadly the structyre will look like:\n", "\n", "```bash\n", " backend/ # your FastAPI app\n", " main.py\n", " requirements.txt\n", " frontend/ # your Next.js app\n", " package.json\n", " app/\n", "```\n", "\n", "You can just create the backend folder, move the main.py script there, and then execute: \n", "\n", "```bash\n", "npx create-next-app@latest frontend --ts --eslint --tailwind\n", "```\n", "\n", "It scaffolds a ready-to-run Next.js project named `frontend` with TypeScript, ESLint, and Tailwind preconfigured. Breakdown:\n", "\n", "* `npx`: runs the package without installing it globally (grabs the latest `create-next-app`).\n", "* `create-next-app@latest`: the official Next.js project generator (using the newest version).\n", "* `frontend`: the folder/name of your new app.\n", "* `--ts`: sets up **TypeScript** (`tsconfig.json`, `next-env.d.ts`, `.tsx` pages/components).\n", "* `--eslint`: adds **ESLint** with Next.js rules (includes `next/core-web-vitals`) and a `.eslintrc`. ESLint is a static analysis (linting) tool for JavaScript/TypeScript. It scans your code without running it and flags problems—bugs, anti-patterns, style issues, and security foot-guns—based on a set of rules.\n", "* `--tailwind`: installs and wires [**Tailwind CSS**](https://en.wikipedia.org/wiki/Tailwind_CSS) (`tailwind.config.js`, `postcss.config.js`, adds `@tailwind` directives to `globals.css`).\n", "\n", "We get:\n", "\n", "* A Next.js app (App Router by default) with sensible defaults.\n", "* Dependencies installed (`next`, `react`, `react-dom`, `tailwindcss`, `postcss`, `autoprefixer`, ESLint plugins).\n", "* Git initialized (unless disabled), `.gitignore`, `README.md`, and npm scripts in `package.json`.\n", "\n", "After that we get: \n", "\n", "```bash\n", "% tree -L 2 \n", ".\n", "├── backend\n", "│ └── main.py\n", "└── frontend\n", " ├── README.md\n", " ├── eslint.config.mjs\n", " ├── next-env.d.ts\n", " ├── next.config.ts\n", " ├── node_modules\n", " ├── package-lock.json\n", " ├── package.json\n", " ├── postcss.config.mjs\n", " ├── public\n", " ├── src\n", " └── tsconfig.json\n", "```\n", "\n", "\n", "## Run it\n", "\n", "How to run it:\n", "\n", "```bash\n", "cd frontend\n", "npm run dev\n", "```\n", "\n", "This starts the dev server on **[http://localhost:3000](http://localhost:3000)**.\n", "\n", "\n", "It tells us: \n", "\n", "`To get started, edit the page.tsx file.`\n", "\n", "This file is in `frontend/src/app/page.tsx`. You can play around modifying the text.\n", "\n", "\n", "## Bringing endpoints in\n", "\n", "Now we want to call our backend from the frontend. \n", "\n", "We add `CORS`:\n", "\n", "```python\n", "# fastapi main.py\n", "from fastapi.middleware.cors import CORSMiddleware\n", "\n", "app.add_middleware(\n", " CORSMiddleware,\n", " allow_origins=[\"http://localhost:3000\"], # your Next.js dev URL\n", " allow_credentials=True,\n", " allow_methods=[\"*\"],\n", " allow_headers=[\"*\"],\n", ")\n", "```\n", "\n", "We add an environment variable to link the ports. For that we create: `frontend/.env.local` and write:\n", "\n", "```\n", "NEXT_PUBLIC_API_URL=http://127.0.0.1:8000\n", "```\n", "\n", "Then, in `frontend/src/app/page.tsx`, we paste:\n", "\n", "```tsx\n", "// app/page.tsx\n", "\"use client\";\n", "import { useState } from \"react\";\n", "\n", "type FftResponse = {\n", " k: number; n: number; L: number;\n", " bin_index: number;\n", " ideal_bin_float: number;\n", " bin_offset_from_integer: number;\n", " Y_m_real: number; Y_m_imag: number; Y_m_abs: number;\n", " Y_m_norm_real: number; Y_m_norm_imag: number; Y_m_norm_abs: number;\n", " note: string;\n", "};\n", "\n", "export default function Page() {\n", " const [n, setN] = useState(4096);\n", " const [L, setL] = useState(2 * Math.PI);\n", " const [k, setK] = useState(14);\n", " const [data, setData] = useState(null);\n", " const [loading, setLoading] = useState(false);\n", " const [err, setErr] = useState(null);\n", "\n", " const run = async () => {\n", " try {\n", " setLoading(true); setErr(null);\n", " const url = new URL(`${process.env.NEXT_PUBLIC_API_URL}/fft`);\n", " url.search = new URLSearchParams({\n", " n: String(n),\n", " L: String(L),\n", " k: String(k),\n", " }).toString();\n", "\n", " const res = await fetch(url.toString());\n", " if (!res.ok) throw new Error(await res.text());\n", " const j = (await res.json()) as FftResponse;\n", " setData(j);\n", " } catch (e: any) {\n", " setErr(e.message ?? \"Request failed\");\n", " } finally {\n", " setLoading(false);\n", " }\n", " };\n", "\n", " return (\n", "
\n", "

Signal Lab UI

\n", "\n", "
\n", " \n", " \n", " \n", "
\n", "\n", " \n", " {loading ? \"Computing…\" : \"Compute FFT bin\"}\n", " \n", "\n", " {err &&

{err}

}\n", "\n", " {data && (\n", "
\n",
    "          {JSON.stringify(data, null, 2)}\n",
    "        
\n", " )}\n", "
\n", " );\n", "}\n", "\n", "```\n", "\n", "\n", "## Ports troubleshooting\n", "\n", "You can kill a process using a port as follows:\n", "\n", "\n", "- List the ports:\n", "\n", "```bash\n", " $ lsof -i :3000\n", "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\n", "node 57630 boris 16u IPv6 0x87525578ebb38ad4 0t0 TCP *:hbci (LISTEN)\n", "```\n", "\n", "- kill the process:\n", "\n", "```bash\n", "kill 57630 # ask it to terminate nicely\n", "# or, if it won’t die:\n", "kill -9 57630 # force kill (SIGKILL)\n", "```\n" ] }, { "cell_type": "markdown", "id": "67e4f446-54f5-4ad8-9b19-19caf60d7142", "metadata": {}, "source": [ "# Deploying an app\n", "\n", "See the relevant files here:\n", "\n", "- Dockerfile for the backend [[link]](https://github.com/borisbolliet/ResearchComputing/blob/main/docs/fastapi/app_project/backend/Dockerfile)\n", "- Dockerfile for the frontend [[link]](https://github.com/borisbolliet/ResearchComputing/blob/main/docs/fastapi/app_project/frontend/Dockerfile)\n", "- Docker compose configuration [[link]](https://github.com/borisbolliet/ResearchComputing/blob/main/docs/fastapi/app_project/docker-compose.yml)\n", "\n", "The Docker compose configuration launches both containers for the frontend and backend **services**.\n", "\n", "You should have shell scripts that orchestrate these files. \n", "\n", "For instance, such that you can do: \n", "\n", "```bash\n", "./scripts/docker-start.sh\n", "```\n", "\n", "To launch the Docker container. Inside this script, you would have commands like:\n", "\n", "```bash\n", "docker compose up \n", "```\n", "\n", "to launch your services. Or \n", "\n", "```bash\n", "docker compose up -d\n", "```\n", "\n", "with the `-d` option to run them in the background, so your terminal is freed; with Docker just printing container IDs and exits.\n", "\n", "etc.\n" ] } ], "metadata": { "kernelspec": { "display_name": "c1_base_env", "language": "python", "name": "c1_base_env" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.9" } }, "nbformat": 4, "nbformat_minor": 5 }