Skip to content

Commit 5ddd562

Browse files
authored
Merge pull request #12 from cardamomcode/fast-api
feat: Add fast-api chapter
2 parents e277983 + ab2b29d commit 5ddd562

6 files changed

Lines changed: 382 additions & 15 deletions

File tree

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ This is a comprehensive guide to [Fable.Python](https://github.com/fable-compile
1212
4. **Interop** - Using existing Python libraries and Fable.Python bindings
1313
5. **Bindings** - Creating your own type-safe bindings for Python libraries
1414
6. **Compatibility** - Supported F# features and limitations
15-
7. **Fable v5** - What's new in Fable v5 for Python
16-
8. **Libraries** - Existing ecosystem (Thoth.Json, AsyncRx, Siren, etc.) *(coming soon)*
17-
9. **Pydantic** - Pydantic interop with Decorate and ClassAttributes
18-
10. **Units of Measure** - Compile-time dimensional analysis
19-
11. **Testing** - Testing F# code with Python test runners
20-
12. **Fable.Literate** - The self-documenting converter
21-
13. **Summary** - Wrap-up, resources, and contributing
15+
7. **Async Programming** - F# async and Python asyncio
16+
8. **Testing** - Testing F# code with Python test runners
17+
9. **Fable v5** - What's new in Fable v5 for Python
18+
10. **Pydantic** - Pydantic interop with Decorate and ClassAttributes
19+
11. **FastAPI** - Building type-safe web APIs with F#
20+
12. **Units of Measure** - Compile-time dimensional analysis
21+
13. **Fable.Literate** - The self-documenting converter
22+
14. **Summary** - Wrap-up, resources, and contributing
2223

2324
## The Strange Loop
2425

chapters/FastAPI.fs

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
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

Comments
 (0)