Skip to main content

rAPIdy - a fast, lightweight, and modern asynchronous web framework powered by aiohttp and pydantic.

Project description

rAPIdy

Package version Python versions license license Pydantic V1 Pydantic V2

rAPIdy - a fast, lightweight, and modern asynchronous web framework powered by aiohttp and pydantic.

🚀 Why rAPIdy?

rAPIdy is designed for developers who need a fast, async-first web framework that combines the performance of aiohttp with the simplicity and modern features of frameworks like FastAPI.

Simple rAPIdy server:

from rapidy import Rapidy
from rapidy.http import get

@get("/")
async def hello() -> dict[str, str]:
    return {"message": "Hello, rAPIdy!"}

app = Rapidy(http_route_handlers=[hello])

🔥 Key Features

  • Fast & Lightweight – Minimal overhead, built on aiohttp
  • Async-First – Fully asynchronous by design
  • Built-in Validation – Uses pydantic for request validation
  • Simple & Flexible – Supports both rAPIdy-style handler definitions and traditional aiohttp function-based and class-based routing
  • Middleware Support – Easily extend functionality with middleware, including built-in validation for HTTP parameters (headers, cookies, and body).

📦 Installation & Setup

Install rAPIdy via pip:

pip install rapidy

📄 Documentation

Documentation: https://rapidy.dev


📢 Updates Channel

Updates Channel: https://t.me/rapidy_dev


🏁 Quickstart: First Simple Server

Simple rAPIdy server:

Copy the following code into a file named main.py:

from rapidy import Rapidy
from rapidy.http import get

@get("/")
async def hello() -> dict[str, str]:
    return {"message": "Hello, rAPIdy!"}

rapidy = Rapidy(http_route_handlers=[hello])

Server Startup

There are several ways to start the server:

  • Using run_app
  • Using WSGI (gunicorn)

Using run_app

Example: Copy the following code to `main.py`:
from rapidy import Rapidy, run_app
from rapidy.http import get

@get("/")
async def hello() -> dict[str, str]:
    return {"message": "Hello, rAPIdy!"}

rapidy = Rapidy(http_route_handlers=[hello])

if __name__ == '__main__':
    run_app(rapidy, host="0.0.0.0", port=8000)

Run the server in real-time:

python3 main.py

Your API will be available at http://localhost:8000 🚀


Using WSGI (gunicorn)

Example: Gunicorn is a Python WSGI HTTP server for UNIX.

Install gunicorn for your project:

pip install gunicorn

Copy the following code to main.py:

from rapidy import Rapidy
from rapidy.http import get

@get("/")
async def hello() -> dict[str, str]:
    return {"message": "Hello, rAPIdy!"}

rapidy = Rapidy(http_route_handlers=[hello])

Run the following command in the terminal:

gunicorn main:rapidy --bind localhost:8000 --reload --worker-class aiohttp.GunicornWebWorker
  • main: Name of the main.py file (Python module).
  • rapidy: Object created inside main.py (line: rapidy = Rapidy()).
  • --reload: Restarts the server when code changes are detected. Recommended only for development purposes.

Your API will be available at http://localhost:8000 🚀


📌 Advanced Features

1️⃣ Routing

Define multiple routes with function-based or class-based views:

Functional Handlers:

from rapidy.http import post, Body

@post("/items")
async def create_item(item: dict[str, str] = Body()) -> dict[str, str]:
    return item

[!TIP]

Registering without decorators:
from rapidy import Rapidy
from rapidy.http import post, Body

async def create_item(item: dict[str, str] = Body()) -> dict[str, str]:
    return item

rapidy = Rapidy()
rapidy.add_http_routers([post.reg('/items', create_item)])

Class-Based Handlers:

All methods decorated with @get, @post, etc., are automatically registered as sub-routes.

from rapidy.http import controller, get, post, put, patch, delete, PathParam, Body

@controller('/')
class ItemController:
    @get('/{item_id}')
    async def get_by_id(self, item_id: str = PathParam()) -> dict[str, str]:
        return {'hello': 'rapidy'}

    @get()
    async def get_all(self) -> list[dict[str, str]]:
        return [{'hello': 'rapidy'}, {'hello': 'rapidy'}]

    @post()
    async def post(self, item: dict[str, str] = Body()) -> dict[str, str]:
        return item

    @put()
    async def put(self, item: dict[str, str] = Body()) -> dict[str, str]:
        return item

    @patch()
    async def patch(self, item: dict[str, str] = Body()) -> dict[str, str]:
        return item

    @delete()
    async def delete(self, item: dict[str, str] = Body()) -> dict[str, str]:
        return item

[!TIP]

Registering without decorators:
from rapidy import Rapidy
from rapidy.http import PathParam, get, post, put, patch, delete, controller, Body

class ItemController:
    @get('/{item_id}')
    async def get_by_id(self, item_id: str = PathParam()) -> dict[str, str]:
        return {'hello': 'rapidy'}

    @get()
    async def get_all(self) -> list[dict[str, str]]:
        return [{'hello': 'rapidy'}, {'hello': 'rapidy'}]

    @post()
    async def post(self, item: dict[str, str] = Body()) -> dict[str, str]:
        return item

    @put()
    async def put(self, item: dict[str, str] = Body()) -> dict[str, str]:
        return item

    @patch()
    async def patch(self, item: dict[str, str] = Body()) -> dict[str, str]:
        return item

    @delete()
    async def delete(self, item: dict[str, str] = Body()) -> dict[str, str]:
        return item

rapidy = Rapidy(
    http_route_handlers=[controller.reg('/', ItemController)],
)

You can register a controller in a router without a decorator, but the methods still need to be wrapped with decorators.

2️⃣ Request Validation

rAPIdy uses pydantic not only to validate request data but also for response data serialization. This ensures data consistency and type safety throughout the entire request-response cycle:

from rapidy.http import post, Body
from pydantic import BaseModel

class ItemSchema(BaseModel):
    name: str
    price: float

@post("/items")
async def create_item(data: ItemSchema = Body()) -> ItemSchema:
    return data

If validation fails, a 422 Unprocessable Entity response with error details is automatically returned.

[!TIP]

Example:
from pydantic import BaseModel, Field
from rapidy import Rapidy
from rapidy.http import PathParam, Body, Request, Header, StreamResponse, middleware, post
from rapidy.typedefs import CallNext

TOKEN_REGEXP = '^[Bb]earer (?P<token>[A-Za-z0-9-_=]*)'

class RequestBody(BaseModel):
    username: str = Field(min_length=2, max_length=50)
    password: str = Field(min_length=2, max_length=50)

class ResponseBody(BaseModel):
    hello: str = 'rapidy'

@middleware
async def get_bearer_middleware(
        request: Request,
        call_next: CallNext,
        bearer_token: str = Header(alias='Authorization', pattern=TOKEN_REGEXP),
) -> StreamResponse:
    # process token here ...
    return await call_next(request)

@post('/{user_id}')
async def handler(
        user_id: str = PathParam(),
        body: RequestBody = Body(),
) -> str:
    return 'success'

app = Rapidy(
    middlewares=[get_bearer_middleware],
    http_route_handlers=[handler],
)

Successful Request Validation:

curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer my-token" \
-d '{"username": "Username", "password": "Password"}' \
-v http://127.0.0.1:8080/1
< HTTP/1.1 200 OK ...
success

Failed Request Validation:

curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer my-token" \
-d '{"username": "U", "password": "P"}' \
-v http://127.0.0.1:8080/1
< HTTP/1.1 422 Unprocessable Entity ...
{
    "errors": [
        {
            "type": "string_too_short",
            "loc": ["body", "username"],
            "msg": "String should have at least 2 characters",
            "ctx": {"min_length": 2}
        },
        {
            "type": "string_too_short",
            "loc": ["body", "password"],
            "msg": "String should have at least 2 characters",
            "ctx": {"min_length": 2}
        }
    ]
}

3️⃣ Middleware Support

Easily add powerful middleware for authentication, logging, and more. rAPIdy allows you to validate HTTP parameters (headers, cookies, body) directly within middleware for enhanced flexibility:

Middleware can be used for authentication, logging, or other cross-cutting concerns.

from rapidy import Rapidy
from rapidy.http import PathParam, Request, Header, StreamResponse, middleware, post
from rapidy.typedefs import CallNext

TOKEN_REGEXP = '^[Bb]earer (?P<token>[A-Za-z0-9-_=]*)'

@middleware
async def get_bearer_middleware(
        request: Request,
        call_next: CallNext,
        bearer_token: str = Header(alias='Authorization', pattern=TOKEN_REGEXP),
) -> StreamResponse:
    # process token here ...
    return await call_next(request)

@post('/{user_id}')
async def handler(user_id: str = PathParam()) -> str:
    return 'success'

app = Rapidy(
    middlewares=[get_bearer_middleware],
    http_route_handlers=[handler],
)

[!NOTE] Middleware functions must always take Request as the first argument and call_next as the second argument.


4️⃣ Dependency Injection

Rapidy uses the dishka library as its built-in Dependency Injection (DI) mechanism.

We aimed to choose a DI library aligned with the philosophy of Rapidy: simplicity, speed, transparency, and scalability. dishka perfectly fits these principles, offering developers a powerful tool without unnecessary complexity.

In Rapidy, dishka is available out-of-the-box — no additional setup required.

from rapidy import Rapidy
from rapidy.http import Request, StreamResponse, get, middleware
from rapidy.typedefs import CallNext
from rapidy.depends import provide, Provider, Scope, FromDI

class FooProvider(Provider):
    @provide(scope=Scope.REQUEST)
    async def some_obj(self) -> int:
        return 1

@middleware
async def some_middleware(
    request: Request,
    call_next: CallNext,
    some_obj: FromDI[int],
) -> StreamResponse:
    print({"value": some_obj})
    return await call_next(request)

@get('/')
async def handler(some_obj: FromDI[int]) -> dict:
    return {"value": some_obj}

app = Rapidy(
    middlewares=[some_middleware],
    http_route_handlers=[handler],
    di_providers=[FooProvider()],
)

To gain a deeper understanding of how the DI mechanism works, refer to the documentation for Rapidy and dishka.


5️⃣ Lifespan Support

Lifespan is a lifecycle manager for background tasks within Rapidy.

Although aiohttp supports the background tasks feature, rapidy does it more conveniently.

Lifespan manages tasks that should be started: before or after server startup, or should always run.

There are several ways to start tasks: on_startup, on_shutdown, on_cleanup, lifespan.


on_startup

on_startup - tasks that will be executed in the event loop along with the application's request handlers immediately after the application starts.

from rapidy import Rapidy

def startup() -> None:
    print('startup')

rapidy = Rapidy(on_startup=[startup])
Additional examples:
from rapidy import Rapidy

def startup(rapidy: Rapidy) -> None:
    print(f'startup, application: {rapidy}')

rapidy = Rapidy(on_startup=[startup])
from rapidy import Rapidy

async def async_startup() -> None:
    print('async_startup')

rapidy = Rapidy(on_startup=[async_startup])
from rapidy import Rapidy

async def async_startup(rapidy: Rapidy) -> None:
    print(f'async_startup, application: {rapidy}')

rapidy = Rapidy(on_startup=[async_startup])

Adding on_startup to an already created Application object.

rapidy = Rapidy()
rapidy.lifespan.on_startup.append(startup)

on_shutdown

on_shutdown - tasks that will be executed after the server stops.

from rapidy import Rapidy

def shutdown() -> None:
    print('shutdown')

rapidy = Rapidy(on_shutdown=[shutdown])
Additional examples:
from rapidy import Rapidy

def shutdown(rapidy: Rapidy) -> None:
    print(f'shutdown, application: {rapidy}')

rapidy = Rapidy(on_shutdown=[shutdown])
from rapidy import Rapidy

async def async_shutdown() -> None:
    print('async_shutdown')

rapidy = Rapidy(on_shutdown=[async_shutdown])
from rapidy import Rapidy

async def async_shutdown(rapidy: Rapidy) -> None:
    print(f'async_shutdown, application: {rapidy}')

rapidy = Rapidy(on_shutdown=[async_shutdown])

Adding on_shutdown to an already created Application object.

rapidy = Rapidy()
rapidy.lifespan.on_shutdown.append(shutdown)

on_cleanup

on_cleanup - tasks that will be executed after the server stops and all on_shutdown tasks are completed.

from rapidy import Rapidy

def cleanup() -> None:
    print('cleanup')

rapidy = Rapidy(on_cleanup=[cleanup])
Additional examples:
from rapidy import Rapidy

def cleanup(rapidy: Rapidy) -> None:
    print(f'cleanup, application: {rapidy}')

rapidy = Rapidy(on_cleanup=[cleanup])
from rapidy import Rapidy

async def async_cleanup() -> None:
    print('async_cleanup')

rapidy = Rapidy(on_cleanup=[async_cleanup])
from rapidy import Rapidy

async def async_cleanup(rapidy: Rapidy) -> None:
    print(f'async_cleanup, application: {rapidy}')

rapidy = Rapidy(on_cleanup=[async_cleanup])

Adding on_cleanup to an already created Application object.

rapidy = Rapidy()
rapidy.lifespan.on_cleanup.append(cleanup)

lifespan

lifespan - manages background tasks.

from contextlib import asynccontextmanager
from typing import AsyncGenerator
from rapidy import Rapidy

@asynccontextmanager
async def bg_task() -> AsyncGenerator[None, None]:
    try:
        print('starting background task')
        yield
    finally:
        print('finishing background task')

rapidy = Rapidy(
    lifespan=[bg_task()],
)
Additional example:
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from rapidy import Rapidy

@asynccontextmanager
async def bg_task_with_app(rapidy: Rapidy) -> AsyncGenerator[None, None]:
    try:
        print('starting background task')
        yield
    finally:
        print('finishing background task')

rapidy = Rapidy(
    lifespan=[bg_task_with_app],
)

Adding lifespan to an already created Application object.

rapidy = Rapidy()
rapidy.lifespan.append(bg_task())

🧪 Testing with rAPIdy

You can use pytest and pytest-aiohttp for testing your rAPIdy application:

Install pytest and pytest-aiohttp

pip install pytest pytest-aiohttp

Example of a simple test:

from rapidy import Rapidy
from rapidy.http import get
from pytest_aiohttp.plugin import AiohttpClient

@get('/')
async def hello() -> dict[str, str]:
    return {'message': 'Hello, rAPIdy!'}

async def test_hello(aiohttp_client: AiohttpClient) -> None:
    app = Rapidy(http_route_handlers=[hello])

    client = await aiohttp_client(app)

    resp = await client.get('/')
    assert resp.status == 200
    assert await resp.json() == {'message': 'Hello, rAPIdy!'}

🔄 Migration from aiohttp

rAPIdy is built on top of aiohttp, offering a familiar development experience with powerful enhancements:

  • Full Compatibility with aiohttp Syntax – rAPIdy fully supports the definition of HTTP handlers just like in aiohttp, offering the same capabilities. For more details, refer to the rAPIdy documentation.
  • Cleaner Routing Syntax – No need for web.RouteTableDef, making route definitions more concise and readable.
  • Significantly Reduced Boilerplate Code – rAPIdy minimizes the amount of code required compared to aiohttp, allowing developers to focus on business logic rather than repetitive setup.
  • Built-in Request Validation and Response Serialization – Powered by pydantic, rAPIdy automatically validates incoming requests and serializes responses, ensuring data consistency and reducing potential errors.
  • Powerful Middlewares – First-class support for middleware and easy-to-use dependency injection out of the box.
  • Lifespan support.

🛠️ Mypy Support

rAPIdy includes its own plugin for mypy.

The rapidy.mypy plugin helps with type checking in code that uses rAPIdy.

Add the auxiliary configuration to your mypy configuration file.

mypy.ini

; Example configuration for mypy.ini
; ...
[tool.mypy]
plugins =
    pydantic.mypy
    rapidy.mypy
; ...

pyproject.toml

# Example configuration for pyproject.toml
# ...
[tool.mypy]
plugins = [
    "pydantic.mypy",
    "rapidy.mypy"
]
# ...

🛤️ Roadmap

We're actively improving rAPIdy to make it more powerful and efficient. Stay up-to-date with our latest milestones and future plans by checking out the detailed roadmap below.

You can find the full and detailed roadmap on our GitHub page: ROADMAP.md


🤝 Contributing & Support

Want to improve rAPIdy? We welcome contributions! 🚀


📜 License

rAPIdy is licensed under the MIT License. See LICENSE for details.


Start building fast, reliable, and modern APIs with rAPIdy today! 🚀

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rapidy-1.1.2.tar.gz (77.0 kB view details)

Uploaded Source

Built Distribution

rapidy-1.1.2-py3-none-any.whl (89.8 kB view details)

Uploaded Python 3

File details

Details for the file rapidy-1.1.2.tar.gz.

File metadata

  • Download URL: rapidy-1.1.2.tar.gz
  • Upload date:
  • Size: 77.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.7.1 CPython/3.10.17 Linux/6.11.0-1012-azure

File hashes

Hashes for rapidy-1.1.2.tar.gz
Algorithm Hash digest
SHA256 3d7ba02d912777e52ccffff526e0402439111f4ba1d0ba57c80fb999d8525576
MD5 e0f6ca670039d3e639f21acfefa60318
BLAKE2b-256 921adb05c71c5fb22fd88baccf49444d361945d1e64b469ae8c099f84d8cb4f5

See more details on using hashes here.

File details

Details for the file rapidy-1.1.2-py3-none-any.whl.

File metadata

  • Download URL: rapidy-1.1.2-py3-none-any.whl
  • Upload date:
  • Size: 89.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.7.1 CPython/3.10.17 Linux/6.11.0-1012-azure

File hashes

Hashes for rapidy-1.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f929c8e708243ae5241569df408848efc80862993ea3e7ac0760519b1a095fd4
MD5 be01413b5378836879d14a01c04ccc67
BLAKE2b-256 5611c7bba39b88e1fc0d277320c4bb5add11e93bcc5f8d66cc25a89fce85cb41

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page