the main goal of this framework is to make expandable system for creating api with minimal dependencies and overhead
Key features:
- Easy to use: Syntax was inspired by your favourite framework
- Fast: contains only 1 dependency with minimal overhead see benchmarks
- Async Only: Supports only work with async requests handling
- Validation: Built-in support of MsgSpec providing first-class Validation Speed
- Minimalistic: Framework contains Minimal functional to build API
- msgspec: A fast serialization and validation library
pip install rapis
pip install granian # install any rsgi compatible web-server
# or simply
pip install rapis[standard] # includes granian in requirements# main.py
from rapis import App, Router
router = Router()
@router.get("/")
async def root() -> dict:
return {}
app = App(router)granian main:app# routes.py
from msgspec import Struct
from rapis import MsgSpecParser, Router
router = Router()
# parameters without another source are read from the query string
router.parser = MsgSpecParser()
class Item(Struct):
name: str
@router.get("/items/{item_id}")
async def get_item(item_id: int, verbose: bool = False) -> dict:
# item_id comes from the path, verbose from the query string
return {"id": item_id, "verbose": verbose}
@router.post("/items", status=201)
@MsgSpecParser("body") # the JSON body is validated into Item
async def create_item(item: Item) -> Item:
return item# main.py
from rapis import App
from routes import router
app = App(router)Resolved once, when the app is built, in this order:
- annotated as
Request— the request itself; - named like a
{placeholder}of the path — converted to the annotated type; - listed in some middleware's
provides— taken fromrequest.state; - a parser:
MsgSpecParser("query")(default) orMsgSpecParser("body"); a parser withfields=[...]takes exactly those names, a parser withoutfieldstakes the rest. Set it per handler as a decorator or per router withrouter.parser = ...(inherited by child routers); - otherwise the parameter needs a default value — a missing source raises
BuildErrorat startup, not at request time.
Return values are encoded as JSON with the status of the route; -> None answers 204, a returned Response(status, headers, body) is sent as is.
Request and Response are msgspec.Structs: == compares their fields, not identity, and they are unhashable, so they cannot be used as dict keys or in sets.
from rapis import (
App,
CORSMiddleware,
Handler,
Header,
HTTPError,
Request,
Response,
Router,
)
root = Router()
# outer middlewares live on the root router only and see every request, 404 included
root.outer_middlewares.append(CORSMiddleware(allow_origins=["https://example.com"]))
class Auth:
provides = ("user",) # the `user` parameter of handlers comes from request.state
async def __call__(self, handler: Handler, request: Request) -> Response:
request.state["user"] = request.scope.headers.get("x-user", "anonymous")
return await handler(request)
api = Router(prefix="/api")
api.middlewares.append(Auth()) # this router and its children
root.include_router(api)
@api.get("/me")
async def me(user: str) -> dict:
return {"user": user}
@api.route("/items/{item_id}", methods=["PUT", "PATCH"]) # methods without a shortcut
async def update_item(item_id: int) -> None:
raise HTTPError(404, f"item {item_id} not found") # 404 {"detail": "item 3 not found"}
app = App(root)A middleware should return a Response instead of raising. If it raises, its own code after await handler(request) is skipped, so CORS, for example, cannot add headers to that error; the app still turns the exception into a response (HTTPError mapping) or a 500. Errors raised deeper, in handlers or in inner middlewares, are already responses by the time an outer middleware sees them, so outer middlewares decorate them as usual.