Routing and Creating HTTP Handlers
Web handlers are designed to process incoming HTTP requests.
In Rapidy
, routing and handler creation are closely related: routing determines which handler will be invoked in response to a request.
Defining Routes
A route is a URL
string that triggers a handler.
There are two types of routes: static and dynamic.
Difference Between Static and Dynamic Routing
Route Type | Example URL | Description |
---|---|---|
Static | /about | The URL is fixed. |
Dynamic | /users/{user_id} | The URL changes depending on parameters. |
Rapidy
supports multiple ways of defining routes similar to aiohttp
. More details on this are provided below in the section Creating and Registering HTTP Handlers
.
You can learn more about aiohttp
handlers here.
Static Routes
Static HTTP routing is a type of routing where the path (URL) is predefined and does not change dynamically. This means that every request to a specific route always leads to the same handler.
Simple Static Route
from rapidy.http import get
@get('/hello_rapidy')
async def handler() -> str:
return 'Hello Rapidy!'
This route is always available via GET /hello
and returns the same response.
Similarly, you can define other methods such as get
, post
, put
, delete
, and so on.
Dynamic Routes
Dynamic routing allows you to define routes that accept variable parameters.
This is useful when working with different entities (e.g., user_id
, post_id
, etc.) by passing them in the URL
.
The examples below use PathParam
, which is required for extracting path parameters. You can read more about it here.
Simple Dynamic Route
Suppose we have an API to retrieve user information based on user_id
:
from rapidy.http import get, PathParam
@get('/users/{user_id}')
async def handler(user_id: int = PathParam()) -> dict[str, int]:
return {'user_id': user_id}
How does this route work?
user_id
is a dynamic parameter passed in the URL.Rapidy
automatically converts it toint
(if a string is passed, the API will return a422
error).
Example request:
Response:
Dynamic Routes with Multiple Parameters
You can add multiple dynamic parameters:
from rapidy.http import get, PathParam
@get('/posts/{post_id}/comments/{comment_id}')
async def handler(
post_id: int = PathParam(),
comment_id: int = PathParam(),
) -> dict[str, int]:
return {'post_id': post_id, 'comment_id': comment_id}
Now, the request GET /posts/10/comments/5
will return:
Grouping Routes
If you have many routes, you can use one of the available approaches to group HTTP requests.
It is recommended to stick to a single approach within a project.
HTTPRouter
Rapidy
provides an HTTPRouter
object for grouping requests.
HTTPRouter
allows registering groups of handlers and plays a key role in routing by directing requests to the appropriate handlers based on the HTTP method,
path, parameters, and other conditions.
HTTPRouter
is registered just like any other HTTP handler.
from rapidy import Rapidy
from rapidy.http import HTTPRouter, controller, get
@get('/healthcheck') # /healthcheck
async def healthcheck() -> str:
return 'ok'
@get('/hello') # /api/hello
async def hello_handler() -> dict[str, str]:
return {'hello': 'rapidy'}
api_router = HTTPRouter('/api', [hello_handler])
rapidy = Rapidy(http_route_handlers=[healthcheck, api_router])
HTTPRouter
can do more!
HTTPRouter
also has several attributes that extend its capabilities, such as middleware
handling, background task management, and more.
You can also create nested HTTPRouter
instances.
You can read more about HTTPRouter
here.
Creating and Registering HTTP Handlers
Functional Handlers
The simplest way to create a handler:
from rapidy import Rapidy
from rapidy.http import post
@post('/')
async def handler() -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy(http_route_handlers=[handler])
Examples of Handler Registration
Registering a Handler Without a Decorator
Adding a Handler via the Application router
(aiohttp style)
from rapidy import Rapidy
async def handler() -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy()
rapidy.router.add_post('/', handler)
Supported methods correspond to HTTP methods with the add_
prefix.
add_get
add_post
add_put
add_patch
add_delete
Exception — view
.
add_view
Adding a Handler with a Decorator via RouteTableDef
(aiohttp style)
Adding a Handler Without a Decorator via rapidy.web
(aiohttp style)
Class-Based Handlers
Class-based handlers allow grouping multiple methods within a single class:
from rapidy import Rapidy
from rapidy.http import PathParam, controller, get, post, put, patch, delete
@controller('/')
class UserController:
@get('/{user_id}')
async def get_by_id(self, user_id: str = PathParam()) -> dict[str, str]:
return {'user_id': user_id}
@get()
async def get_all_users(self) -> list[dict[str, str]]:
return [{'name': 'John'}, {'name': 'Felix'}]
@post()
async def create_user(self) -> str:
return 'ok'
@put()
async def update_user(self) -> str:
return 'ok'
@patch()
async def patch_user(self) -> str:
return 'ok'
@delete()
async def delete_user(self) -> str:
return 'ok'
rapidy = Rapidy(http_route_handlers=[UserController])
Examples of Class-Based Handler Registration
Registering a Handler Without a Decorator
from rapidy import Rapidy
from rapidy.http import PathParam, controller, get
class UserController:
@get('/{user_id}')
async def get_by_id(self, user_id: str = PathParam()) -> dict[str, str]:
return {'user_id': user_id}
@get()
async def get_all_users(self) -> list[dict[str, str]]:
return [{'name': 'John'}, {'name': 'Felix'}]
rapidy = Rapidy(
http_route_handlers=[
controller.reg('/', UserController),
]
)
Using View
(aiohttp style)
Adding a Handler via the Application router
(aiohttp style)
from rapidy import Rapidy
from rapidy.web import View
class Handler(View):
async def get(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy()
rapidy.router.add_view('/', Handler)
Adding a Handler via router
with Different Paths (aiohttp style)
from rapidy import Rapidy
from rapidy.web import View, PathParam
class Handler(View):
async def get(self, user_id: str = PathParam()) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy()
rapidy.router.add_get('/{user_id}', Handler)
rapidy.router.add_view('/', Handler)
Adding a Handler with a Decorator via RouteTableDef
(aiohttp style)
from rapidy import web
routes = web.RouteTableDef()
@routes.view('/')
class Handler(web.View):
async def get(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = web.Application()
rapidy.add_routes(routes)
Adding a Handler with a Decorator via RouteTableDef
with Different Paths (aiohttp style)
from rapidy import web
routes = web.RouteTableDef()
@routes.view('/')
class Handler(web.View):
@routes.get('/{user_id}')
async def get(self, user_id: str = web.PathParam()) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = web.Application()
rapidy.add_routes(routes)
Adding a Handler Without a Decorator via rapidy.web
(aiohttp style)
from rapidy import web
routes = web.RouteTableDef()
class Handler(web.View):
async def get(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = web.Application()
rapidy.add_routes([web.view('/', Handler)])
Adding a Handler Without a Decorator via rapidy.web
with Different Paths (aiohttp style)
from rapidy import web
routes = web.RouteTableDef()
class Handler(web.View):
async def get(self, user_id: str = web.PathParam()) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = web.Application()
rapidy.add_routes([
web.get('/{user_id}', Handler),
web.view('/', Handler),
])
Handler Attributes
Attributes allow managing handler behavior and responses.
Attributes are automatically applied to handler responses if the handler returns anything other than Response
(does not apply to path
and allow_head
attributes for the get
method).
Attributes are applied to responses.
The response_content_type
attribute will be applied to each handler response
because the handler returns a python
object.
Attributes are not applied to responses.
The response_content_type
attribute will not be applied to the handler response
because the handler returns a low-level Response
object.
All handler creation methods support the same attributes for managing web requests.
Core Attributes (Always Applied)
path
path
: str
— the handler's route on the server.
allow_head
allow_head
: bool = True
— if set to True
(default), a route is added for the head
method with the same handler as get
.
This attribute can only be applied to the get
method.
Response Validation
response_validate
response_validate
: bool = True
— whether to validate the handler response.
@get(
'/',
response_validate=False,
)
async def handler() -> str: # <-- `str` will be ignored
return {'hello': 'rapidy'}
response_type
response_type
: Type[Any] | None = ...
— defines the response type (overrides return annotation).
@get(
'/',
response_type=dict[str, str], # <-- `dict[str, str]` will be used for validation
)
async def handler() -> str: # <-- `str` will be ignored
return {'hello': 'rapidy'}
This flag adds flexibility for serialization and validation but is rarely used.
Managing Headers and Encoding
response_content_type
response_content_type
: str = 'application/json'
— an attribute that allows managing the Content-Type
header.
The Content-Type
header informs the client (browser, API client, another server) about the type of data contained in the HTTP response body.
from rapidy.enums import ContentType
@get(
'/',
response_content_type=ContentType.text_plain,
)
async def handler() -> str:
return 'hello, rapidy!'
If content_type
is specified, the provided data will be converted accordingly.
If content_type
is not specified, it will be determined automatically based on the type of data returned by the server.
content_type="application/json
content_type="application/json"
— data is converted to JSON
using jsonify(dumps=True)
and encoded according to response_charset.
content_type="text/*
content_type="text/*"
(any text type: text/plain
, text/html
, etc.) - if the data is of type str
, it is sent as is.
Otherwise, it is converted to a string via jsonify(dumps=False).
from rapidy.http import get, ContentType
@get(
'/',
content_type=ContentType.text_any,
)
async def handler() -> str:
return 'hello rapidy!' # "hello rapidy!"
If the object is still not a string after jsonify(dumps=False)
, it is further converted using response_json_encoder.
content_type - any other MIME type.
If the data is of type bytes
, it is sent as is.
Otherwise, it is converted to a string using jsonify(dumps=True) and encoded according to response_json_encoder.
If content_type
is not specified, it is set automatically:
-
body: dict | BaseModel | dataclass
→content_type="application/json"
-
body: str | Enum | int | float | Decimal | bool
→content_type="text/plain"
-
body: Any
→content_type="application/octet-stream"
response_charset
response_charset
: str = 'utf-8'
— the encoding used for response data.
from rapidy.enums import Charset
@get(
'/',
response_charset=Charset.utf8,
)
async def handler() -> str:
return 'hello, rapidy!'
response_json_encoder
response_json_encoder
: Callable = json.dumps
— a function that takes an object and returns its JSON representation.
Automatically applied to any Python object after validation through pydantic
.
from typing import Any
def custom_encoder(obj: Any) -> str:
...
@get(
'/',
response_json_encoder=custom_encoder, # Converts the obtained string above into a JSON object using the `custom_encoder` function
)
async def handler() -> dict[str, str]:
return {'hello': 'rapidy!'} # will be converted to a string by Rapidy's internal tools
Data Compression
response_zlib_executor
response_zlib_executor
: concurrent.futures.Executor | None = None
— zlib
compression function.
from concurrent.futures import Executor
class SomeExecutor(Executor):
...
@get(
'/',
response_zlib_executor=SomeExecutor,
)
async def handler() -> str:
return 'hello, rapidy!'
More about zlib_executor
zlib_executor
is an aiohttp
mechanism. More details here.
response_zlib_executor_size
response_zlib_executor_size
: int | None = None
— body size in bytes to enable compression.
Managing Pydantic Fields
response_include_fields
response_include_fields
: set[str] | dict[str, Any] | None = None
— include
parameter from Pydantic
, specifying which fields to include.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
another_value: str = Field('another_data', alias='someAnotherValue')
@get(
'/',
response_include_fields={'value'},
)
async def handler() -> Result:
return Result() # {'someValue': 'data'}
response_exclude_fields
response_exclude_fields
: set[str] | dict[str, Any] | None
— list of fields to exclude.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
another_value: str = Field('another_data', alias='someAnotherValue')
@get(
'/',
response_exclude_fields={'value'},
)
async def handler() -> Result:
return Result() # {"someValue": "data"}
response_by_alias
response_by_alias
: bool = True
— whether to use Pydantic
aliases.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
@get(
'/',
response_by_alias=True, # <-- default
)
async def handler() -> Result:
return Result() # {"someValue": "data"}
...
@get(
'/',
response_by_alias=False,
)
async def handler() -> Result:
return Result() # {"value": "data"}
response_exclude_unset
response_exclude_unset
: bool = False
— whether to exclude default values.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
another_value: str = Field('another_data', alias='someAnotherValue')
@get(
'/',
exclude_unset=False, # <-- default
)
async def handler() -> Result:
Result(someAnotherValue='new_data') # {"someValue": "data", "someAnotherValue": "new_data"}
...
@get(
'/',
exclude_unset=True,
)
async def handler() -> Result:
return Result(someAnotherValue='new_data') # {"someAnotherValue": "new_data"}
response_exclude_defaults
response_exclude_defaults
: bool = False
— whether to exclude explicitly set values if they match the defaults.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
@get(
'/',
exclude_defaults=False, # <-- default
)
async def handler() -> Result:
return Result() # {"value": "data"}
...
@get(
'/',
exclude_defaults=True,
)
async def handler() -> Result:
return Result() # {}
response_exclude_none
response_exclude_none
: bool = False
— whether to exclude None
values.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
none_value: None = None
@get(
'/',
exclude_none=False, # <-- default
)
async def handler() -> Result:
return Result() # {"someValue": "data", "none_value": null}
...
@get(
'/',
exclude_none=True,
)
async def handler() -> Result:
return Result() # {"someValue": "data"}
response_custom_encoder
response_custom_encoder
: Callable | None = None
— custom_encoder
parameter from Pydantic
, allowing a custom encoder to be specified.