Registering Routes#

At the root of every Litestar application there is an instance of the Litestar class, on which the root level controllers, routers, and route handler functions are registered using the route_handlers kwarg:

Registering route handlers#
from litestar import Litestar, get

def sub_path_handler() -> None: ...

def root_handler() -> None: ...

app = Litestar(route_handlers=[root_handler, sub_path_handler])

Components registered on the app are appended to the root path. Thus, the root_handler function will be called for the path "/", whereas the sub_path_handler will be called for "/sub-path".

You can also declare a function to handle multiple paths, e.g.:

Registering a route handler for multiple paths#
from litestar import get, Litestar

@get(["/", "/sub-path"])
def handler() -> None: ...

app = Litestar(route_handlers=[handler])

To handle more complex path schemas you should use controllers and routers

Registering routes dynamically#

Occasionally there is a need for dynamic route registration. Litestar supports this via the register method exposed by the Litestar app instance:

Registering a route handler dynamically with the register method#
from litestar import Litestar, get

def root_handler() -> None: ...

app = Litestar(route_handlers=[root_handler])

def sub_path_handler() -> None: ...


Since the app instance is attached to all instances of ASGIConnection, Request, and WebSocket objects, you can in effect call the register() method inside route handler functions, middlewares, and even injected dependencies. For example:

Call the register() method from inside a route handler function#
from typing import Any
from litestar import Litestar, Request, get

def route_handler(request: Request[Any, Any]) -> None:
   def sub_path_handler() -> None: ...


app = Litestar(route_handlers=[route_handler])

In the above we dynamically created the sub_path_handler and registered it inside the route_handler function.


Although Litestar exposes the register method, it should not be abused. Dynamic route registration increases the application complexity and makes it harder to reason about the code. It should therefore be used only when absolutely required.


Routers are instances of the Router, class which is the base class for the Litestar app itself.

A Router can register Controllers, route handler functions, and other routers, similarly to the Litestar constructor:

Registering a Router#
from litestar import Litestar, Router, get

def order_handler(order_id: int) -> None: ...

order_router = Router(path="/orders", route_handlers=[order_handler])
base_router = Router(path="/base", route_handlers=[order_router])
app = Litestar(route_handlers=[base_router])

Once order_router is registered on base_router, the handler function registered on order_router will become available on /base/orders/{order_id}.


Controllers are subclasses of the Controller class. They are used to organize endpoints under a specific sub-path, which is the controller’s path. Their purpose is to allow users to utilize Python OOP for better code organization and organize code by logical concerns.

Click to see an example of registering a controller
Registering a Controller#
from litestar.contrib.pydantic import PydanticDTO
from litestar.controller import Controller
from litestar.dto import DTOConfig, DTOData
from litestar.handlers import get, post, patch, delete
from pydantic import BaseModel, UUID4

class UserOrder(BaseModel):
   user_id: int
   order: str

class PartialUserOrderDTO(PydanticDTO[UserOrder]):
   config = DTOConfig(partial=True)

class UserOrderController(Controller):
   path = "/user-order"

   async def create_user_order(self, data: UserOrder) -> UserOrder: ...

   async def retrieve_user_order(self, order_id: UUID4) -> UserOrder: ...

   @patch(path="/{order_id:uuid}", dto=PartialUserOrderDTO)
   async def update_user_order(
       self, order_id: UUID4, data: DTOData[PartialUserOrderDTO]
   ) -> UserOrder: ...

   async def delete_user_order(self, order_id: UUID4) -> None: ...

The above is a simple example of a “CRUD” controller for a model called UserOrder. You can place as many route handler methods on a controller, as long as the combination of path+http method is unique.

The path that is defined on the controller is appended before the path that is defined for the route handlers declared on it. Thus, in the above example, create_user_order has the path of the controller - /user-order/, while retrieve_user_order has the path /user-order/{order_id:uuid}".


If you do not declare a path class variable on the controller, it will default to the root path of "/".

Registering components multiple times#

You can register both standalone route handler functions and controllers multiple times.


Registering a controller multiple times#
from litestar import Router, Controller, get

class MyController(Controller):
   path = "/controller"

   def handler(self) -> None: ...

internal_router = Router(path="/internal", route_handlers=[MyController])
partner_router = Router(path="/partner", route_handlers=[MyController])
consumer_router = Router(path="/consumer", route_handlers=[MyController])

In the above, the same MyController class has been registered on three different routers. This is possible because what is passed to the router is not a class instance but rather the class itself. The router creates its own instance of the controller, which ensures encapsulation.

Therefore, in the above example, three different instances of MyController will be created, each mounted on a different sub-path, e.g., /internal/controller, /partner/controller, and /consumer/controller.

Route handlers#

You can also register standalone route handlers multiple times:

Registering a route handler multiple times#
from litestar import Litestar, Router, get

def my_route_handler() -> None: ...

internal_router = Router(path="/internal", route_handlers=[my_route_handler])
partner_router = Router(path="/partner", route_handlers=[my_route_handler])
consumer_router = Router(path="/consumer", route_handlers=[my_route_handler])

Litestar(route_handlers=[internal_router, partner_router, consumer_router])

When the handler function is registered, it’s actually copied. Thus, each router has its own unique instance of the route handler. Path behaviour is identical to that of controllers above, namely, the route handler function will be accessible in the following paths: /internal/handler, /partner/handler, and /consumer/handler.


You can nest routers as you see fit - but be aware that once a router has been registered it cannot be re-registered or an exception will be raised.

Mounting ASGI Apps#

Litestar support “mounting” ASGI applications on sub-paths, i.e., specifying a handler function that will handle all requests addressed to a given path.

Click to see an example of mounting an ASGI app
Mounting an ASGI App#
import json
from typing import TYPE_CHECKING

from litestar import Litestar, asgi
from litestar.response.base import ASGIResponse

    from litestar.types import Receive, Scope, Send

@asgi("/some/sub-path", is_mount=True)
async def my_asgi_app(scope: "Scope", receive: "Receive", send: "Send") -> None:
        scope: The ASGI connection scope.
        receive: The ASGI receive function.
        send: The ASGI send function.

    body = json.dumps({"forwarded_path": scope["path"]})
    response = ASGIResponse(body=body.encode("utf-8"))
    await response(scope, receive, send)

app = Litestar(route_handlers=[my_asgi_app])

The handler function will receive all requests with an url that begins with /some/sub-path, e.g, /some/sub-path, /some/sub-path/abc, /some/sub-path/123/another/sub-path, etc.

Technical Details

If we are sending a request to the above with the url /some/sub-path, the handler will be invoked and the value of scope["path"] will equal "/`". If we send a request to /some/sub-path/abc, it will also be invoked,and scope["path"] will equal "/abc".

Mounting is especially useful when you need to combine components of other ASGI applications - for example, for third party libraries. The following example is identical in principle to the one above, but it uses Starlette:

Click to see an example of mounting a Starlette app
Mounting a Starlette App#
from typing import TYPE_CHECKING

from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route

from litestar import Litestar, asgi

    from starlette.requests import Request

async def index(request: "Request") -> JSONResponse:
    """A generic starlette handler."""
    return JSONResponse({"forwarded_path": request.url.path})

starlette_app = asgi(path="/some/sub-path", is_mount=True)(
            Route("/", index),
            Route("/abc/", index),
            Route("/123/another/sub-path/", index),

app = Litestar(route_handlers=[starlette_app])

Why Litestar uses radix based routing

The regex matching used by popular frameworks such as Starlette, FastAPI or Flask is very good at resolving path parameters fast, giving it an advantage when a URL has a lot of path parameters - what we can think of as vertical scaling. On the other hand, it is not good at scaling horizontally - the more routes, the less performant it becomes. Thus, there is an inverse relation between performance and application size with this approach that strongly favors very small microservices. The trie based approach used by Litestar is agnostic to the number of routes of the application giving it better horizontal scaling characteristics at the expense of somewhat slower resolution of path parameters.

Litestar implements its routing solution that is based on the concept of a radix tree or trie.

See also

If you are interested in the technical aspects of the implementation, refer to this GitHub issue - it includes an indepth discussion of the pertinent code.