Running the marimo backend programmatically¶
marimo can be run programmatically using the marimo
module. This is useful when you want to run marimo as part of a larger application or when you need to customize the behavior of marimo (e.g. middleware, custom error handling, authentication, routing, etc).
FastAPI Example¶
Here's an example of how you can run a marimo application programmatically using FastAPI:
from typing import Annotated, Callable, Coroutine
from fastapi.responses import HTMLResponse, RedirectResponse
import marimo
from fastapi import FastAPI, Form, Request, Response
# Create a marimo asgi app
server = (
marimo.create_asgi_app()
.with_app(path="", root="./pages/index.py")
.with_app(path="/dashboard", root="./pages/dashboard.py")
.with_app(path="/sales", root="./pages/sales.py")
)
# Create a FastAPI app
app = FastAPI()
app.add_middleware(auth_middleware)
app.add_route("/login", my_login_route, methods=["POST"])
app.mount("/", server.build())
# Run the server
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)
For a more complete example, see the FastAPI example.
Note that when run in this mode, marimo will serve its static assets under the name of the notebook (in the example above, that would be http://hostname/<dashboard|sales>/assets/<assetname.css|js|...>
). If you are using custom authorization middleware, skip authentication for these assets to avoid server round-trips. There are many of them.
Dynamic directory¶
If you'd like to create a server to dynamically load marimo notebooks from a directory, you can use the with_dynamic_directory
method. This is useful if the contents of the directory change often, such as a directory of notebooks for a dashboard, without restarting the server.
server = (
marimo.create_asgi_app()
.with_dynamic_directory(path="/dashboard", directory="./notebooks")
)
If the notebooks in the directory are expected to be static, it is better to use the with_app
method and loop through the directory contents.
from pathlib import Path
server = marimo.create_asgi_app()
app_names: list[str] = []
notebooks_dir = Path(__file__).parent / "notebooks"
for filename in sorted(notebooks_dir.iterdir()):
if filename.suffix == ".py":
app_name = filename.stem
server = server.with_app(path=f"/{app_name}", root=filename)
app_names.append(app_name)
Accessing Request Data¶
Inside your marimo notebooks, you can access the current request data using mo.app_meta().request
. This is particularly useful when implementing authentication or accessing user data.
import marimo as mo
# Access request data in your notebook
request = mo.app_meta().request
if request and request.user and request.user["is_authenticated"]:
content = f"Welcome {request.user['username']}!"
else:
content = "Please log in"
mo.md(content)
Authentication Middleware Example¶
Here's an example of how to implement authentication middleware that populates request.user
:
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Add user data to the request scope
# This will be accessible via mo.app_meta().request.user
request.scope["user"] = {
"is_authenticated": True,
"username": "example_user",
# Add any other user data
}
# Optional add metadata to the request
request.scope["meta"] = {
"some_key": "some_value",
}
response = await call_next(request)
return response
# Add the middleware to your FastAPI app
app.add_middleware(AuthMiddleware)
The request
object provides access to:
request.headers
: Request headersrequest.cookies
: Request cookiesrequest.query_params
: Query parametersrequest.path_params
: Path parametersrequest.user
: User data added by authentication middlewarerequest.url
: URL information including path, query parametersrequest.meta
: Metadata added by your custom middleware
Documenting and Validating Query Parameters¶
When mounted apps accept query parameters, it can be helpful to declare, validate, and document them with the help of a Pydantic model.
If a marimo app called notebooks/items.py
is mounted to /items
, declaring an endpoint with the same route will take the query parameters through Pydantic model validation first, then redirect to the marimo endpoint.
# src/main.py
from fastapi import FastAPI, Request, Query
from fastapi.responses import RedirectResponse
from marimo import create_asgi_app
from pathlib import Path
from pydantic import BaseModel, Field
from typing import Annotated, Literal
from urllib.parse import urlencode
app = FastAPI()
class FilterParams(BaseModel):
limit: int = Field(100, gt=0, le=100)
offset: int = Field(0, ge=0)
order_by: Literal["created_at", "updated_at"] = "created_at"
tags: list[str] = []
@app.get("/items")
async def marimo_items(
request: Request, filter_query: Annotated[FilterParams, Query()]
):
query_params = urlencode(filter_query.model_dump(), doseq=True)
return RedirectResponse(url=f"/items/?{query_params}")
server = create_asgi_app(include_code=True, quiet=False)
notebooks_dir = Path(__file__).parent.parent / "notebooks"
for filename in notebooks_dir.iterdir():
if filename.suffix == ".py":
app_name = filename.stem
server = server.with_app(path=f"/{app_name}", root=filename)
app.mount("/", server.build())
Under the Hood¶
Behind the scenes, in this mode, marimo is spinning up a new computational kernel in a separate sub-thread (same process) for each new session / app created. There are a few implications of this from a performance and reliability perspective:
- If you are running multiple instances of this same server for load balancing, you will need to use sticky sessions in your load balancer to ensure that the same client gets the same kernel each time.
- Similarly, attempting to run multiple instances of the same FastAPI process (a common approach with Python web services) on the same node will not work reliably, since only one of them will actually be running the kernel.
In summary, there are limitations to how far the approach described here can horizontally scale, so we recommend scaling vertically first. In other words, increase the container CPU/Memory specs before increasing the number of container instances.