Skip to main content

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

License: MIT Python Versions Unit Tests Type Checking Linting codecov Downloads Docs GitHub last commit Repo Size

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

  1. Introduction
  2. Defining Controllers
  3. Middleware
  4. Data Validation
  5. Authentication
  6. Error Handling
  7. 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:

    1. Inherit from the Controller class
    2. 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 from BaseController, 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


Download files

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

Source Distribution

birchrest-1.0.8.tar.gz (40.9 kB view details)

Uploaded Source

Built Distribution

birchrest-1.0.8-py3-none-any.whl (50.5 kB view details)

Uploaded Python 3

File details

Details for the file birchrest-1.0.8.tar.gz.

File metadata

  • Download URL: birchrest-1.0.8.tar.gz
  • Upload date:
  • Size: 40.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.8.18

File hashes

Hashes for birchrest-1.0.8.tar.gz
Algorithm Hash digest
SHA256 e149c7ce72d89ae96668a586286701c23a0d4c5630a7341e6899985b1a91aa3f
MD5 5ddc623751269a5a8179ac607285dc28
BLAKE2b-256 3c645417976184ef5da4713c9b2ec93ed121450da9f610ca3f16d1b9aac0e046

See more details on using hashes here.

File details

Details for the file birchrest-1.0.8-py3-none-any.whl.

File metadata

  • Download URL: birchrest-1.0.8-py3-none-any.whl
  • Upload date:
  • Size: 50.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.8.18

File hashes

Hashes for birchrest-1.0.8-py3-none-any.whl
Algorithm Hash digest
SHA256 ed1e0c8b86a98b8a9f9c298bc5a0b8c3d7c83f07c11f88a600e1dd1a9179c020
MD5 7cc2584611d1c2d4aac49e790cddfae3
BLAKE2b-256 aa80fc8595208c4fe62c7b28510d2bb648e7476d20e5e1bc62e6373b4442bc1a

See more details on using hashes here.

Supported by

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