Skip to content

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 headers
  • request.cookies: Request cookies
  • request.query_params: Query parameters
  • request.path_params: Path parameters
  • request.user: User data added by authentication middleware
  • request.url: URL information including path, query parameters
  • request.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.