A lightweight Python framework for building REST APIs with a controller-based approach. It includes built-in middleware, error handling, auth handling, automatic response management and more.
Project description
BirchRest
BirchRest is a simple, lightweight framework for setting up RESTful APIs with minimal configuration. It is designed to be intuitive and flexible.
Full documentation is available here: https://alexandengstrom.github.io/birchrest
Quickstart
You can install the latest version of birchrest using pip:
pip install birchrest
Init a boilerplate project with birch init command:
birch init
Table of Contents
- Introduction
- Defining Controllers
- Middleware
- Data Validation
- Authentication
- Error Handling
- Unit Testing
Introduction
BirchRest follows a controller-based architecture, where each controller represents a logical grouping of API routes. The framework automatically constructs your API at runtime from the controllers you define. To make this work, simply create a file named __birch__.py
and import all your controllers into this file. BirchRest will use this file to discover and configure your API routes.
from birchrest import Controller
from birchrest.decorators import get, controller
from birchrest.http import Request, Response
@controller("api")
class MyController(Controller):
@get("hello")
async def hello(self, req: Request, res: Response):
return res.send({"message": "Hello from the app!"})
To start the server, instantiate the BirchRest class and call its serve method.
from birchrest import BirchRest
app = BirchRest()
app.serve()
Or start the server via command line:
birch serve --port [PORT] --host [HOST] --log-level [LOG_LEVEL]
Defining Controllers
In Birchrest, controllers are the building blocks of your API. Each controller defines multiple endpoints, and controllers can be nested to create hierarchical routes.
Key Concepts
-
Base Path: Each controller has a base path that defines where its routes are accessible. If a controller has subcontrollers, their base paths are combined, creating a nested structure.
-
Controller Setup: To create a controller:
- Inherit from the Controller class
- Use the @controller decorator on the class, passing the base path as an argument.
Defining Endpoints
Inside a controller, use HTTP method decorators like @get or @post to define endpoints. These decorators can take an optional path to extend the controller’s base path for that specific route.
# Create an endpoint that accepts PATCH method on route /myendpoint.
@patch("myendpoint")
async def patch(self, req: Request, res: Response):
print(req.body)
return res.send({"message": "success"})
To define path variables, use a colon (:
) in the path. You can then access these variables through the req.params
object.
@get("user/:id")
async def patch(self, req: Request, res: Response):
userId = req.params.get("id")
return res.send({"id": userId})
A route can also access queries in the same way:
@get("user")
async def patch(self, req: Request, res: Response):
name = req.queries.get("name")
return res.send({"name": name})
It is possible to set automatic contraints for the body, queries and params via validation decorators. See section about validation.
Nesting Controllers
BirchRest supports hierarchical route structures by allowing controllers to inherit from other controllers. This creates nested routes where the child controller's base path is combined with the parent controller's base path. In BirchRest, subcontrollers are created by having one controller class inherit from another controller class.
This approach makes it easy to group related endpoints under a common path and manage them as a logical structure.
Example
Let’s say we have a base API controller and we want to nest a resource controller under it:
from birchrest import Controller
from birchrest.decorators import get, controller
from birchrest.http import Request, Response
# Define the parent controller
@controller("api")
class BaseController(Controller):
@get("status")
async def status(self, req: Request, res: Response):
return res.send({"message": "API is running"})
# Define the child controller that inherits from the parent
@controller("resource")
class ResourceController(BaseController):
@get("hello")
async def hello(self, req: Request, res: Response):
return res.send({"message": "Hello from resource!"})
This will create the endpoint /api/resouce/hello.
In this example:
- The
BaseController
is the parent controller that handles routes under/api
. - The
ResourceController
inherits fromBaseController
, making it a child controller nested under/api/resource
. - The route for the "hello" endpoint in
ResourceController
becomes/api/resource/hello
. - The route for the "status" endpoint from
BaseController
is/api/status
.
By inheriting from BaseController, the ResourceController becomes a child, automatically inheriting and extending the parent’s routing structure.
Middleware
Middleware allows you to perform tasks before or after a request is processed by a controller, such as logging, modifying the request, or checking permissions. Birchrest provides built-in middleware for common tasks and the ability to define your own custom middleware.
Custom Middlewares
You can create custom middleware to handle specific logic or modify request and response objects. This section explains how to define and register middleware in your application.
Middleware operates hierarchically, meaning it applies to all routes below the point where it’s defined. You can set up global middleware directly at the application level, or use decorators on controllers and routes. When applied to a controller, the middleware will affect all routes within that controller, as well as any nested controllers attached to it. If applied to a route it will be applied only on that route.
Requirements
A middleware should be a class that inherits from the Middleware class and it must implement an async call method. The call method will receive a Request, Response and NextFunction. If the NextFunction is called the call will continue to the next middleware or route handler. If not called, we wont continue. The next function must be awaited.
from birchrest.http import Request, Response, Middleware
from birchrest.types import NextFunction
from birchrest.exceptions import BadRequest
class MyMiddleware(Middleware):
def __init__(self, state: int):
self.state = state
async def __call__(self, req: Request, res: Response, next: NextFunction):
if self.state:
await next()
else:
raise BadRequest
It is possible to execute things after next is called aswell, this means you can use middlewares for postprocessing aswell.
Built-in Middlewares
Birchrest comes with several built-in middleware options that help manage common use cases, such as request logging, rate limiting or CORS support. These can be easily added to your API with minimal configuration. These can be imported from the middlewares module.
from birchrest.middlewares import Cors, Logger, RateLimiter
Data Validation
Data validation in Birchrest is supported via Python data classes. This allows for strict validation of request data (body, queries, and params) to ensure that all incoming data adheres to the expected structure.
To be able to use validation, you must also define the models. Example:
@dataclass
class Address:
street: str = field(metadata={"min_length": 3, "max_length": 100})
city: str = field(metadata={"min_length": 2, "max_length": 50})
@dataclass
class User:
username: str = field(metadata={"min_length": 3, "max_length": 20})
email: str = field(metadata={"regex": r"^[\w\.-]+@[\w\.-]+\.\w+$"})
age: int = field(metadata={"min_value": 0, "max_value": 120})
address: Address
You can then use the @body, @queries and @params decorator with the dataclass as argument.
Example:
@post("user")
@body(User)
async def create_user(self, req: Request, res: Response):
# It is safe to pass the body directly since we have already validated it.
save_to_database(request.body)
return res.status(201).send()
If the validation fails, the user will get an automatic response. For example, if we try to post a user to the route above but passes a username with only two letters. We will receive this response:
{
"error": {
"status": 400,
"code": "Bad Request",
"correlationId": "67ad2218-262e-478b-b767-04cfafd4315b",
"message": "Body validation failed: Field 'username' must have at least 3 characters."
}
}
Read more about how automatic error responses are handled in the error section.
Query and URL Param Validation
Validating queries and params is done in the same way, just use the @queries and @params decorators instead.
Authentication
Birchrest makes it easy to protect your API routes with authentication mechanisms. It allows you to define custom authentication handlers and easily mark routes as protected, ensuring that only authenticated requests are allowed access.
Custom Auth Handlers
You can define your own authentication handler to manage how users are authenticated in your system. Once defined, Birchrest will handle the integration with the API. If your route handler returns a falsy value or raises an Exception, the execution will be stopped. Otherwise the return value from this function will be put under the user property in the request object. It is therefore possible to put information there that tells you which user sent a request.
Protecting Routes
You can easily protect individual routes or entire controllers by marking them as requiring authentication. Birchrest will automatically handle unauthorized access by returning predefined error messages.
from birchrest import BirchRest, Controller
from birchrest.decorators import get, controller
from birchrest.http import Request, Response
async def auth_handler(req: Request, res: Response):
if req.headers.get("Authorization"):
# Do your logic
return { "id": 1 }
return False
@controller("api")
class MyController(Controller):
@protected()
@get("protected")
async def hello(self, req: Request, res: Response):
return res.send({"message": "Hello from the app!"})
app = BirchRest()
app.register(MyController)
app.serve()
Error Handling
By default, BirchRest responds with standardized error messages and provides as much detail as possible when an error occurs. Common error responses like 404 (Not Found) when a route doesn't exist, or 400 (Bad Request) when body validation fails, are handled automatically. If an unhandled exception occurs within your controllers, a 500 Internal Server Error will be returned.
ApiError
The ApiError class is the base class for a variety of HTTP exceptions such as NotFound, BadRequest, Unauthorized, and more. If any of these exceptions are raised during request handling, BirchRest will automatically convert them into the appropriate HTTP response with the correct status code and error message.
from birchrest.exceptions import NotFound
raise NotFound
This will automatically generate a 404 Not Found HTTP response to the client, with the provided user-friendly message.
Each ApiError has the following attributes:
status_code
: The HTTP status code (e.g., 404, 400, 500).base_message
: A default message associated with the status code (e.g., "Not Found" for 404).user_message
: An optional custom message that can provide more specific details about the error.
BirchRest supports the following common HTTP exceptions out-of-the-box:
-
BadRequest
(400) -
Unauthorized
(401) -
Forbidden
(403) -
NotFound
(404) -
MethodNotAllowed
(405) -
Conflict
(409) -
UnprocessableEntity
(422) -
InternalServerError
(500) -
ServiceUnavailable
(503) -
PaymentRequired
(402) -
RequestTimeout
(408) -
Gone
(410) -
LengthRequired
(411) -
PreconditionFailed
(412) -
PayloadTooLarge
(413) -
UnsupportedMediaType
(415) -
TooManyRequests
(429) -
UpgradeRequired
(426)
The framework handles everything behind the scenes if any of these exceptions are raised. You don't need to manually craft the response or worry about setting the correct status code—BirchRest takes care of it.
Custom Error Handler
If you need more control over how errors are handled, you can define your own custom error handler. This handler will receive the request, response, and exception as arguments. The handler must manage the exception explicitly; otherwise, a 500 Internal Server Error
will be returned by default.
Unit Testing
To simplify testing, the framework includes a test adapter class that simulates sending HTTP requests to your API. This allows you to test everything except the server itself, with all middlewares, authentication handlers, and other components functioning exactly as they would in a real request. The adapter returns the final response object, which you can inspect and assert in your tests.
The TestAdapter class takes an instance of your app and then provides methods like get, post etc that accepts a path, headers and body.
from birchrest import BirchRest
from birchrest.unittest import TestAdapter
app = BirchRest()
runner = TestAdapter(app)
response = runner.get("/your-route")
BirchRest also provides a custom TestCase class to make it easier to assert responses, example:
import unittest
from birchrest import BirchRest
from birchrest.unittest import TestAdapter, BirchRestTestCase
class ApiTest(BirchRestTestCase):
def setUp(self) -> None:
app = BirchRest(log_level="test")
self.runner = TestAdapter(app)
async def test_user_route(self) -> None:
response = await self.runner.get("/user")
self.assertOk(response)
async def test_invalid_id(self) -> None:
response = await self.runner.get("/user/0")
self.assertNotOk(response)
self.assertStatus(response, 400)
if __name__ == "__main__":
unittest.main()
Contributing
Contributions are welcome! Please refer to the CONTRIBUTING.md file for details on how to get involved, submit pull requests, and report issues.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file birchrest-1.0.7.tar.gz
.
File metadata
- Download URL: birchrest-1.0.7.tar.gz
- Upload date:
- Size: 40.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.1.1 CPython/3.8.18
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 487bf07d9af4dbb9476121d17d6b23081333c164047f97cfbdb6277d440e8c50 |
|
MD5 | 75250753a7fb981c8fc72c156a71c834 |
|
BLAKE2b-256 | a782cb3c34a4378e79f05f8c45da96164de7ded9f94023fb4870edcf431bee53 |
File details
Details for the file birchrest-1.0.7-py3-none-any.whl
.
File metadata
- Download URL: birchrest-1.0.7-py3-none-any.whl
- Upload date:
- Size: 48.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.1.1 CPython/3.8.18
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 06957d66431bde76d4fe28ccca00cb33bb9f3312010167192e8ab12545139b0d |
|
MD5 | cc9263a376afd1bf855cc3d691c19ebb |
|
BLAKE2b-256 | 8f05ecb51cfd60f409865ee77624ef2f1e0bdd9cd6e70261bc316536ad1e5864 |