|
| 1 | +(** |
| 2 | +# FastAPI |
| 3 | +
|
| 4 | +As an F# developer you are probably familiar with web frameworks like ASP.NET Core, Giraffe, or Oxpecker. But Fable.Python |
| 5 | +also allows you to build web APIs that run in Python environments, using the popular FastAPI framework. |
| 6 | +
|
| 7 | +## What is FastAPI? |
| 8 | +
|
| 9 | +[FastAPI](https://fastapi.tiangolo.com/) is Python's most popular modern web framework. |
| 10 | +It's fast, easy to use, and built on top of Pydantic for automatic request validation |
| 11 | +and OpenAPI documentation. |
| 12 | +
|
| 13 | +FastAPI gives you: |
| 14 | +
|
| 15 | +- **High performance** - One of the fastest Python frameworks available |
| 16 | +- **Automatic validation** - Request/response validation via Pydantic, that you already know from |
| 17 | + the previous chapter |
| 18 | +- **Type hints** - Leverages Python type hints for better editor support |
| 19 | +- **OpenAPI docs** - Interactive Swagger UI and ReDoc generated automatically |
| 20 | +- **Async support** - Native async/await for high concurrency |
| 21 | +
|
| 22 | +Fable.Python includes bindings for FastAPI, allowing you to write type-safe APIs |
| 23 | +using F# while leveraging Python's mature web ecosystem. |
| 24 | +
|
| 25 | +## Setting Up |
| 26 | +
|
| 27 | +Add FastAPI and uvicorn to your Python environment: |
| 28 | +
|
| 29 | +```bash |
| 30 | +uv add fastapi uvicorn |
| 31 | +``` |
| 32 | +
|
| 33 | +Then import the FastAPI module in your F# code: |
| 34 | +*) |
| 35 | + |
| 36 | +(*** hide ***) |
| 37 | +module FastAPI |
| 38 | + |
| 39 | +open System.Threading.Tasks |
| 40 | +open Fable.Core |
| 41 | +open Fable.Python.FastAPI |
| 42 | +open Fable.Python.Pydantic |
| 43 | + |
| 44 | +(** |
| 45 | +```fsharp |
| 46 | +open Fable.Python.FastAPI |
| 47 | +open Fable.Python.Pydantic |
| 48 | +``` |
| 49 | +
|
| 50 | +## Creating the Application |
| 51 | +
|
| 52 | +Create a FastAPI application instance at the module level: |
| 53 | +*) |
| 54 | + |
| 55 | +let app = FastAPI(title = "My API", version = "1.0.0") |
| 56 | + |
| 57 | +(** |
| 58 | +This generates: |
| 59 | +
|
| 60 | +```python |
| 61 | +app = FastAPI(title="My API", version="1.0.0") |
| 62 | +``` |
| 63 | +
|
| 64 | +The `app` variable name is important - the route decorators reference it. |
| 65 | +
|
| 66 | +## Defining Models |
| 67 | +
|
| 68 | +Request and response models use Pydantic's `BaseModel` (covered in the previous chapter): |
| 69 | +*) |
| 70 | + |
| 71 | +[<Py.ClassAttributes(style = Py.ClassAttributeStyle.Attributes, init = false)>] |
| 72 | +type Item(Id: int, Name: string, Price: float, InStock: bool) = |
| 73 | + inherit BaseModel() |
| 74 | + member val Id: int = Id with get, set |
| 75 | + member val Name: string = Name with get, set |
| 76 | + member val Price: float = Price with get, set |
| 77 | + member val InStock: bool = InStock with get, set |
| 78 | + |
| 79 | +[<Py.ClassAttributes(style = Py.ClassAttributeStyle.Attributes, init = false)>] |
| 80 | +type CreateItemRequest(Name: string, Price: float, InStock: bool) = |
| 81 | + inherit BaseModel() |
| 82 | + member val Name: string = Name with get, set |
| 83 | + member val Price: float = Price with get, set |
| 84 | + member val InStock: bool = InStock with get, set |
| 85 | + |
| 86 | +(** |
| 87 | +## Defining Endpoints |
| 88 | +
|
| 89 | +### The APIClass Pattern |
| 90 | +
|
| 91 | +FastAPI endpoints are defined using a class with decorated static methods: |
| 92 | +*) |
| 93 | + |
| 94 | +let items = ResizeArray<Item>() |
| 95 | + |
| 96 | +[<APIClass>] |
| 97 | +type API() = |
| 98 | + /// GET /items - List all items |
| 99 | + [<Get("/items")>] |
| 100 | + static member get_items() : ResizeArray<Item> = |
| 101 | + items |
| 102 | + |
| 103 | + /// GET /items/{item_id} - Get item by ID |
| 104 | + [<Get("/items/{item_id}")>] |
| 105 | + static member get_item(item_id: int) : Task<obj> = task { |
| 106 | + match items |> Seq.tryFind (fun i -> i.Id = item_id) with |
| 107 | + | Some item -> return item :> obj |
| 108 | + | None -> return {| error = "Item not found" |} |
| 109 | + } |
| 110 | + |
| 111 | + /// POST /items - Create a new item |
| 112 | + [<Post("/items")>] |
| 113 | + static member create_item(request: CreateItemRequest) : Task<obj> = task { |
| 114 | + let newId = |
| 115 | + if items.Count = 0 then 1 |
| 116 | + else (items |> Seq.map (fun i -> i.Id) |> Seq.max) + 1 |
| 117 | + let newItem = Item(newId, request.Name, request.Price, request.InStock) |
| 118 | + items.Add(newItem) |
| 119 | + return {| status = "created"; item = newItem |} |
| 120 | + } |
| 121 | + |
| 122 | +(** |
| 123 | +This generates Python with proper FastAPI decorators: |
| 124 | +*) |
| 125 | + |
| 126 | +(*** include-python: API ***) |
| 127 | + |
| 128 | +(** |
| 129 | +### Key Points |
| 130 | +
|
| 131 | +- `[<APIClass>]` marks the class for FastAPI routing. We use a class because Fable |
| 132 | + can only apply decorator attributes to types and methods, not standalone functions |
| 133 | +- Route decorators: `[<Get>]`, `[<Post>]`, `[<Put>]`, `[<Delete>]`, `[<Patch>]` |
| 134 | +- Path parameters use `{param_name}` syntax and map to function arguments |
| 135 | +- Pydantic models in parameters are automatically validated |
| 136 | +- Return types can be sync or async (`Task<'T>`) |
| 137 | +
|
| 138 | +### Anonymous Records for Quick Responses |
| 139 | +
|
| 140 | +F# anonymous records compile to Python dictionaries, perfect for JSON responses: |
| 141 | +*) |
| 142 | + |
| 143 | +[<APIClass>] |
| 144 | +type HealthAPI() = |
| 145 | + [<Get("/health")>] |
| 146 | + static member health() = |
| 147 | + {| status = "healthy"; version = "1.0.0" |} |
| 148 | + |
| 149 | +(** |
| 150 | +## Async Endpoints |
| 151 | +
|
| 152 | +For I/O-bound operations, use `task { }` to create async endpoints: |
| 153 | +*) |
| 154 | + |
| 155 | +[<APIClass>] |
| 156 | +type AsyncAPI() = |
| 157 | + [<Get("/slow")>] |
| 158 | + static member slow_operation() = task { |
| 159 | + // Simulate async work (e.g., database query) |
| 160 | + do! Task.Delay(100) |
| 161 | + return {| message = "Done!" |} |
| 162 | + } |
| 163 | + |
| 164 | +(** |
| 165 | +The `task { }` computation expression compiles to Python's `async def`, |
| 166 | +integrating naturally with FastAPI's async support. |
| 167 | +
|
| 168 | +## Path and Query Parameters |
| 169 | +
|
| 170 | +### Path Parameters |
| 171 | +
|
| 172 | +Path parameters are extracted from the URL: |
| 173 | +*) |
| 174 | + |
| 175 | +[<APIClass>] |
| 176 | +type UsersAPI() = |
| 177 | + [<Get("/users/{user_id}")>] |
| 178 | + static member get_user(user_id: int) = |
| 179 | + {| id = user_id; name = "User " + string user_id |} |
| 180 | + |
| 181 | + [<Get("/users/{user_id}/posts/{post_id}")>] |
| 182 | + static member get_user_post(user_id: int, post_id: int) = |
| 183 | + {| user_id = user_id; post_id = post_id |} |
| 184 | + |
| 185 | +(** |
| 186 | +### Query Parameters |
| 187 | +
|
| 188 | +Query parameters are function arguments not in the path: |
| 189 | +*) |
| 190 | + |
| 191 | +[<APIClass>] |
| 192 | +type SearchAPI() = |
| 193 | + [<Get("/search")>] |
| 194 | + static member search(q: string, limit: int) = |
| 195 | + {| query = q; limit = limit |} |
| 196 | + |
| 197 | +(** |
| 198 | +A request to `/search?q=hello&limit=10` maps to `search("hello", 10)`. |
| 199 | +
|
| 200 | +## Request Bodies |
| 201 | +
|
| 202 | +POST/PUT/PATCH endpoints receive request bodies as Pydantic models: |
| 203 | +*) |
| 204 | + |
| 205 | +[<Py.ClassAttributes(style = Py.ClassAttributeStyle.Attributes, init = false)>] |
| 206 | +type CreateUserRequest(name: string, email: string) = |
| 207 | + inherit BaseModel() |
| 208 | + member val name: string = name with get, set |
| 209 | + member val email: string = email with get, set |
| 210 | + |
| 211 | +[<APIClass>] |
| 212 | +type UserCrudAPI() = |
| 213 | + [<Post("/users")>] |
| 214 | + static member create_user(request: CreateUserRequest) = |
| 215 | + // FastAPI automatically validates the request body |
| 216 | + {| status = "created"; name = request.name; email = request.email |} |
| 217 | + |
| 218 | +(** |
| 219 | +FastAPI validates the incoming JSON against the Pydantic model and returns |
| 220 | +a 422 error if validation fails. |
| 221 | +
|
| 222 | +## HTTP Exceptions |
| 223 | +
|
| 224 | +Return proper HTTP errors using `HTTPException`: |
| 225 | +*) |
| 226 | + |
| 227 | +[<APIClass>] |
| 228 | +type ErrorAPI() = |
| 229 | + [<Get("/protected")>] |
| 230 | + static member protected_route() = |
| 231 | + // Check authentication (simplified example) |
| 232 | + let isAuthenticated = false |
| 233 | + if not isAuthenticated then |
| 234 | + raise (System.Exception("Not authenticated")) |
| 235 | + {| message = "Secret data" |} |
| 236 | + |
| 237 | +(** |
| 238 | +In practice, you would use FastAPI's dependency injection for authentication. |
| 239 | +The `HTTPException` type is available for more idiomatic error handling: |
| 240 | +
|
| 241 | +```fsharp |
| 242 | +// For proper HTTP exceptions, use a helper that emits Python's raise |
| 243 | +[<Emit("raise HTTPException(status_code=$0, detail=$1)")>] |
| 244 | +let raiseHttp (code: int) (msg: string) : unit = nativeOnly |
| 245 | +
|
| 246 | +// Then in your endpoint: |
| 247 | +if not isAuthenticated then |
| 248 | + raiseHttp 401 "Not authenticated" |
| 249 | +``` |
| 250 | +
|
| 251 | +
|
| 252 | +## Running the Application |
| 253 | +
|
| 254 | +Compile with Fable and run with uvicorn: |
| 255 | +
|
| 256 | +```bash |
| 257 | +# Compile F# to Python |
| 258 | +dotnet fable --lang python --outDir build |
| 259 | +
|
| 260 | +# Run the server |
| 261 | +cd build |
| 262 | +uvicorn app:app --reload |
| 263 | +``` |
| 264 | +
|
| 265 | +Visit: |
| 266 | +
|
| 267 | +- `http://localhost:8000` - Your API |
| 268 | +- `http://localhost:8000/docs` - Interactive Swagger UI |
| 269 | +- `http://localhost:8000/redoc` - ReDoc documentation |
| 270 | +
|
| 271 | +## Development Workflow |
| 272 | +
|
| 273 | +For hot-reloading during development, run Fable in watch mode: |
| 274 | +
|
| 275 | +```bash |
| 276 | +# Terminal 1: Watch F# files |
| 277 | +dotnet fable --lang python --outDir build --watch |
| 278 | +
|
| 279 | +# Terminal 2: Run uvicorn with reload |
| 280 | +cd build |
| 281 | +uvicorn app:app --reload |
| 282 | +``` |
| 283 | +
|
| 284 | +Changes to your F# code automatically recompile and uvicorn picks up the changes. |
| 285 | +
|
| 286 | +## Complete Example |
| 287 | +
|
| 288 | +Here's a minimal but complete FastAPI application: |
| 289 | +
|
| 290 | +```fsharp |
| 291 | +module App |
| 292 | +
|
| 293 | +open System.Threading.Tasks |
| 294 | +open Fable.Core |
| 295 | +open Fable.Python.FastAPI |
| 296 | +open Fable.Python.Pydantic |
| 297 | +
|
| 298 | +// Create the app |
| 299 | +let app = FastAPI(title = "Todo API", version = "1.0.0") |
| 300 | +
|
| 301 | +// Define the model |
| 302 | +[<Py.ClassAttributes(style = Py.ClassAttributeStyle.Attributes, init = false)>] |
| 303 | +type Todo(id: int, title: string, completed: bool) = |
| 304 | + inherit BaseModel() |
| 305 | + member val id: int = id with get, set |
| 306 | + member val title: string = title with get, set |
| 307 | + member val completed: bool = completed with get, set |
| 308 | +
|
| 309 | +// In-memory store |
| 310 | +let todos = ResizeArray<Todo>() |
| 311 | +
|
| 312 | +// Define endpoints |
| 313 | +[<APIClass>] |
| 314 | +type TodoAPI() = |
| 315 | + [<Get("/")>] |
| 316 | + static member root() = |
| 317 | + {| message = "Welcome to Todo API" |} |
| 318 | +
|
| 319 | + [<Get("/todos")>] |
| 320 | + static member list_todos() = todos |
| 321 | +
|
| 322 | + [<Post("/todos")>] |
| 323 | + static member create_todo(title: string) = |
| 324 | + let todo = Todo(todos.Count + 1, title, false) |
| 325 | + todos.Add(todo) |
| 326 | + todo |
| 327 | +``` |
| 328 | +
|
| 329 | +## Why F# + FastAPI? |
| 330 | +
|
| 331 | +This combination gives you: |
| 332 | +
|
| 333 | +1. **Compile-time safety** - F# catches errors before they reach Python |
| 334 | +2. **Runtime validation** - Pydantic validates incoming requests |
| 335 | +3. **Auto documentation** - OpenAPI specs generated from your types |
| 336 | +4. **Familiar ecosystem** - Deploy with standard Python tools |
| 337 | +
|
| 338 | +You write type-safe F# code, but deploy and run it like any Python web service. |
| 339 | +*) |
0 commit comments