0
# Web & API Integration
1
2
Modal provides comprehensive web application serving capabilities, enabling deployment of web servers, REST APIs, and integration with popular web frameworks. These decorators transform Modal functions into web endpoints that can handle HTTP requests and serve web applications.
3
4
## Capabilities
5
6
### ASGI Application Support
7
8
Decorator for serving ASGI-compatible applications like FastAPI, Starlette, and modern async web frameworks.
9
10
```python { .api }
11
def asgi_app(func: Callable) -> Callable:
12
"""Decorator to serve ASGI applications (FastAPI, Starlette, etc.)"""
13
```
14
15
#### Usage Examples
16
17
```python
18
import modal
19
from fastapi import FastAPI
20
21
app = modal.App("fastapi-server")
22
web_app = FastAPI()
23
24
@web_app.get("/")
25
def read_root():
26
return {"message": "Hello from Modal FastAPI!"}
27
28
@web_app.get("/items/{item_id}")
29
def read_item(item_id: int, q: str = None):
30
return {"item_id": item_id, "q": q}
31
32
@web_app.post("/process")
33
def process_data(data: dict):
34
# Expensive processing that benefits from Modal's scaling
35
result = expensive_computation(data)
36
return {"result": result}
37
38
@app.function()
39
@modal.asgi_app()
40
def fastapi_app():
41
return web_app
42
43
# The FastAPI app is now served on Modal's infrastructure
44
```
45
46
### WSGI Application Support
47
48
Decorator for serving WSGI-compatible applications like Flask, Django, and traditional web frameworks.
49
50
```python { .api }
51
def wsgi_app(func: Callable) -> Callable:
52
"""Decorator to serve WSGI applications (Flask, Django, etc.)"""
53
```
54
55
#### Usage Examples
56
57
```python
58
import modal
59
from flask import Flask, request, jsonify
60
61
app = modal.App("flask-server")
62
web_app = Flask(__name__)
63
64
@web_app.route("/")
65
def hello():
66
return jsonify({"message": "Hello from Modal Flask!"})
67
68
@web_app.route("/upload", methods=["POST"])
69
def upload_file():
70
if 'file' not in request.files:
71
return jsonify({"error": "No file uploaded"}), 400
72
73
file = request.files['file']
74
# Process file with Modal's compute resources
75
result = process_uploaded_file(file)
76
return jsonify({"processed": result})
77
78
@web_app.route("/predict", methods=["POST"])
79
def make_prediction():
80
data = request.get_json()
81
# Use Modal for ML inference
82
prediction = run_ml_model(data)
83
return jsonify({"prediction": prediction})
84
85
@app.function()
86
@modal.wsgi_app()
87
def flask_app():
88
return web_app
89
90
# The Flask app is now served on Modal's infrastructure
91
```
92
93
### Web Endpoint
94
95
Decorator for creating simple HTTP endpoints without requiring a full web framework.
96
97
```python { .api }
98
def web_endpoint(func: Callable) -> Callable:
99
"""Decorator to create HTTP web endpoints"""
100
```
101
102
#### Usage Examples
103
104
```python
105
import modal
106
import json
107
108
app = modal.App("simple-api")
109
110
@app.function()
111
@modal.web_endpoint(method="GET")
112
def get_status():
113
"""Simple GET endpoint"""
114
return {
115
"status": "healthy",
116
"timestamp": time.time(),
117
"version": "1.0.0"
118
}
119
120
@app.function()
121
@modal.web_endpoint(method="POST")
122
def process_webhook(data=None):
123
"""Handle webhook POST requests"""
124
if data:
125
# Process webhook data
126
result = handle_webhook_data(data)
127
return {"success": True, "processed": result}
128
else:
129
return {"error": "No data provided"}, 400
130
131
@app.function()
132
@modal.web_endpoint(method="GET", path="/metrics")
133
def get_metrics():
134
"""Custom metrics endpoint"""
135
metrics = collect_application_metrics()
136
return {"metrics": metrics}
137
138
# Endpoints are available at:
139
# GET https://your-app.modal.run/get_status
140
# POST https://your-app.modal.run/process_webhook
141
# GET https://your-app.modal.run/metrics
142
```
143
144
### FastAPI Endpoint
145
146
Specialized decorator optimized for FastAPI applications with enhanced integration features.
147
148
```python { .api }
149
def fastapi_endpoint(func: Callable) -> Callable:
150
"""Decorator specifically for FastAPI endpoints with enhanced features"""
151
```
152
153
#### Usage Examples
154
155
```python
156
import modal
157
from fastapi import FastAPI, HTTPException, Depends
158
from pydantic import BaseModel
159
160
app = modal.App("advanced-fastapi")
161
162
class PredictionRequest(BaseModel):
163
text: str
164
model: str = "default"
165
166
class PredictionResponse(BaseModel):
167
prediction: str
168
confidence: float
169
model_used: str
170
171
@app.function()
172
@modal.fastapi_endpoint()
173
def ml_prediction_api():
174
"""Advanced FastAPI endpoint with automatic docs and validation"""
175
fastapi_app = FastAPI(
176
title="ML Prediction API",
177
description="Machine learning predictions powered by Modal",
178
version="1.0.0"
179
)
180
181
@fastapi_app.post("/predict", response_model=PredictionResponse)
182
async def predict(request: PredictionRequest):
183
try:
184
# Load model and make prediction
185
model = load_ml_model(request.model)
186
prediction, confidence = model.predict(request.text)
187
188
return PredictionResponse(
189
prediction=prediction,
190
confidence=confidence,
191
model_used=request.model
192
)
193
except Exception as e:
194
raise HTTPException(status_code=500, detail=str(e))
195
196
@fastapi_app.get("/models")
197
async def list_models():
198
"""List available models"""
199
models = get_available_models()
200
return {"models": models}
201
202
return fastapi_app
203
204
# Automatic OpenAPI docs available at:
205
# https://your-app.modal.run/docs
206
# https://your-app.modal.run/redoc
207
```
208
209
### Web Server
210
211
Decorator for creating custom web servers with full control over the HTTP handling.
212
213
```python { .api }
214
def web_server(func: Callable) -> Callable:
215
"""Decorator to create custom web servers"""
216
```
217
218
#### Usage Examples
219
220
```python
221
import modal
222
from http.server import HTTPServer, BaseHTTPRequestHandler
223
import json
224
225
app = modal.App("custom-server")
226
227
class CustomHandler(BaseHTTPRequestHandler):
228
def do_GET(self):
229
if self.path == "/health":
230
self.send_response(200)
231
self.send_header('Content-type', 'application/json')
232
self.end_headers()
233
response = {"status": "healthy", "server": "custom"}
234
self.wfile.write(json.dumps(response).encode())
235
else:
236
self.send_response(404)
237
self.end_headers()
238
239
def do_POST(self):
240
if self.path == "/process":
241
content_length = int(self.headers['Content-Length'])
242
post_data = self.rfile.read(content_length)
243
244
try:
245
data = json.loads(post_data.decode('utf-8'))
246
result = custom_processing(data)
247
248
self.send_response(200)
249
self.send_header('Content-type', 'application/json')
250
self.end_headers()
251
self.wfile.write(json.dumps({"result": result}).encode())
252
except Exception as e:
253
self.send_response(500)
254
self.end_headers()
255
256
@app.function()
257
@modal.web_server()
258
def custom_web_server():
259
"""Custom HTTP server with full control"""
260
server = HTTPServer(("0.0.0.0", 8000), CustomHandler)
261
server.serve_forever()
262
```
263
264
## Advanced Web Patterns
265
266
### Multi-Framework API Gateway
267
268
```python
269
import modal
270
from fastapi import FastAPI
271
from flask import Flask
272
import asyncio
273
274
app = modal.App("api-gateway")
275
276
# FastAPI for async endpoints
277
fastapi_app = FastAPI()
278
279
@fastapi_app.get("/async-data")
280
async def get_async_data():
281
# Async data processing
282
data = await fetch_external_data()
283
return {"data": data, "type": "async"}
284
285
# Flask for sync endpoints
286
flask_app = Flask(__name__)
287
288
@flask_app.route("/sync-data")
289
def get_sync_data():
290
# Synchronous data processing
291
data = fetch_local_data()
292
return {"data": data, "type": "sync"}
293
294
@app.function()
295
@modal.asgi_app()
296
def async_api():
297
return fastapi_app
298
299
@app.function()
300
@modal.wsgi_app()
301
def sync_api():
302
return flask_app
303
304
# Different apps serve different endpoints
305
# https://your-app-async.modal.run/async-data
306
# https://your-app-sync.modal.run/sync-data
307
```
308
309
### Microservices Architecture
310
311
```python
312
import modal
313
from fastapi import FastAPI
314
315
app = modal.App("microservices")
316
317
# User service
318
user_app = FastAPI(title="User Service")
319
320
@user_app.get("/users/{user_id}")
321
def get_user(user_id: int):
322
return get_user_from_database(user_id)
323
324
@user_app.post("/users")
325
def create_user(user_data: dict):
326
return create_user_in_database(user_data)
327
328
# Order service
329
order_app = FastAPI(title="Order Service")
330
331
@order_app.get("/orders/{order_id}")
332
def get_order(order_id: int):
333
return get_order_from_database(order_id)
334
335
@order_app.post("/orders")
336
def create_order(order_data: dict):
337
# Validate user exists
338
user_id = order_data.get("user_id")
339
user = get_user_from_database(user_id)
340
if not user:
341
raise HTTPException(status_code=404, detail="User not found")
342
343
return create_order_in_database(order_data)
344
345
# Deploy as separate services
346
@app.function()
347
@modal.asgi_app()
348
def user_service():
349
return user_app
350
351
@app.function()
352
@modal.asgi_app()
353
def order_service():
354
return order_app
355
```
356
357
### WebSocket and Real-time Features
358
359
```python
360
import modal
361
from fastapi import FastAPI, WebSocket
362
from typing import List
363
364
app = modal.App("realtime-app")
365
web_app = FastAPI()
366
367
class ConnectionManager:
368
def __init__(self):
369
self.active_connections: List[WebSocket] = []
370
371
async def connect(self, websocket: WebSocket):
372
await websocket.accept()
373
self.active_connections.append(websocket)
374
375
def disconnect(self, websocket: WebSocket):
376
self.active_connections.remove(websocket)
377
378
async def broadcast(self, message: str):
379
for connection in self.active_connections:
380
await connection.send_text(message)
381
382
manager = ConnectionManager()
383
384
@web_app.websocket("/ws")
385
async def websocket_endpoint(websocket: WebSocket):
386
await manager.connect(websocket)
387
try:
388
while True:
389
data = await websocket.receive_text()
390
# Process message and broadcast to all connections
391
processed = process_realtime_data(data)
392
await manager.broadcast(f"Processed: {processed}")
393
except Exception:
394
manager.disconnect(websocket)
395
396
@web_app.post("/broadcast")
397
def broadcast_message(message: dict):
398
"""HTTP endpoint to broadcast messages"""
399
asyncio.create_task(manager.broadcast(json.dumps(message)))
400
return {"status": "broadcasted"}
401
402
@app.function()
403
@modal.asgi_app()
404
def realtime_app():
405
return web_app
406
```
407
408
### File Upload and Processing API
409
410
```python
411
import modal
412
from fastapi import FastAPI, File, UploadFile, HTTPException
413
from typing import List
414
import tempfile
415
import os
416
417
app = modal.App("file-processor")
418
web_app = FastAPI(title="File Processing API")
419
420
@web_app.post("/upload/single")
421
async def upload_single_file(file: UploadFile = File(...)):
422
"""Upload and process a single file"""
423
if not file.filename.endswith(('.jpg', '.png', '.pdf', '.txt')):
424
raise HTTPException(status_code=400, detail="Unsupported file type")
425
426
# Save file temporarily
427
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
428
content = await file.read()
429
tmp_file.write(content)
430
tmp_path = tmp_file.name
431
432
try:
433
# Process file with Modal compute
434
if file.filename.endswith(('.jpg', '.png')):
435
result = process_image(tmp_path)
436
elif file.filename.endswith('.pdf'):
437
result = extract_pdf_text(tmp_path)
438
else:
439
result = process_text_file(tmp_path)
440
441
return {
442
"filename": file.filename,
443
"size": len(content),
444
"processed": result
445
}
446
finally:
447
os.unlink(tmp_path)
448
449
@web_app.post("/upload/batch")
450
async def upload_multiple_files(files: List[UploadFile] = File(...)):
451
"""Upload and process multiple files"""
452
results = []
453
454
for file in files:
455
try:
456
# Process each file
457
content = await file.read()
458
result = await process_file_async(file.filename, content)
459
results.append({
460
"filename": file.filename,
461
"status": "success",
462
"result": result
463
})
464
except Exception as e:
465
results.append({
466
"filename": file.filename,
467
"status": "error",
468
"error": str(e)
469
})
470
471
return {"processed_files": results}
472
473
@web_app.get("/download/{file_id}")
474
async def download_processed_file(file_id: str):
475
"""Download processed file results"""
476
file_path = get_processed_file_path(file_id)
477
if not os.path.exists(file_path):
478
raise HTTPException(status_code=404, detail="File not found")
479
480
return FileResponse(file_path)
481
482
@app.function()
483
@modal.asgi_app()
484
def file_processing_api():
485
return web_app
486
```
487
488
### Authentication and Middleware Integration
489
490
```python
491
import modal
492
from fastapi import FastAPI, Depends, HTTPException, status
493
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
494
from fastapi.middleware.cors import CORSMiddleware
495
import jwt
496
497
app = modal.App("secure-api")
498
web_app = FastAPI(title="Secure API with Authentication")
499
500
# Add CORS middleware
501
web_app.add_middleware(
502
CORSMiddleware,
503
allow_origins=["*"], # Configure appropriately for production
504
allow_credentials=True,
505
allow_methods=["*"],
506
allow_headers=["*"],
507
)
508
509
security = HTTPBearer()
510
511
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
512
"""Verify JWT token"""
513
try:
514
payload = jwt.decode(
515
credentials.credentials,
516
os.environ["JWT_SECRET"],
517
algorithms=["HS256"]
518
)
519
return payload
520
except jwt.ExpiredSignatureError:
521
raise HTTPException(
522
status_code=status.HTTP_401_UNAUTHORIZED,
523
detail="Token expired"
524
)
525
except jwt.InvalidTokenError:
526
raise HTTPException(
527
status_code=status.HTTP_401_UNAUTHORIZED,
528
detail="Invalid token"
529
)
530
531
@web_app.post("/login")
532
def login(credentials: dict):
533
"""Authenticate user and return JWT token"""
534
if authenticate_user(credentials["username"], credentials["password"]):
535
token = create_jwt_token(credentials["username"])
536
return {"access_token": token, "token_type": "bearer"}
537
else:
538
raise HTTPException(
539
status_code=status.HTTP_401_UNAUTHORIZED,
540
detail="Invalid credentials"
541
)
542
543
@web_app.get("/protected")
544
def protected_endpoint(current_user: dict = Depends(verify_token)):
545
"""Protected endpoint requiring authentication"""
546
return {
547
"message": f"Hello {current_user['username']}!",
548
"data": get_user_specific_data(current_user['user_id'])
549
}
550
551
@web_app.get("/admin")
552
def admin_endpoint(current_user: dict = Depends(verify_token)):
553
"""Admin-only endpoint"""
554
if not current_user.get("is_admin"):
555
raise HTTPException(
556
status_code=status.HTTP_403_FORBIDDEN,
557
detail="Admin access required"
558
)
559
560
return {"admin_data": get_admin_data()}
561
562
@app.function(secrets=[modal.Secret.from_name("jwt-secret")])
563
@modal.asgi_app()
564
def secure_api():
565
return web_app
566
```