Skip to main content

Basic Web Backend

A small Python web backend framework designed to work with lema-basic-web-server.

The project provides:

  • static and dynamic routing;
  • HTTP method handling;
  • request parsing;
  • response helper functions;
  • JSON, form and multipart request processing;
  • static file serving;
  • cookies;
  • a lightweight template engine;
  • configurable error handlers;
  • application logging;
  • replaceable request and response adapters.

The framework uses only the Python standard library at runtime.

Requirements

  • Python 3.10 or newer
  • lema-basic-web-server when using the default Lema adapters

Development installation

Clone the repository and install it in editable mode:

git clone https://github.com/lema2000-dev/basic_web_backend.git
cd basic_web_backend
python -m pip install -e ".[dev]"

Run the test suite:

python -m pytest -v

Minimal application

from basic_web_backend import (
    WebApplication,
    html_response,
)
from basic_web_server import Server


app = WebApplication()


@app.route("/")
def index(request):
    return html_response(
        "<h1>Hello from Basic Web Backend!</h1>"
    )


if __name__ == "__main__":
    server = Server(app)
    server.start_console()

After starting the console, use the server's run command to select the listening address and port.

Routing

Routes are registered with the route() decorator:

@app.route("/about")
def about(request):
    return html_response("<h1>About</h1>")

Multiple HTTP methods can be registered:

@app.route(
    "/users",
    methods=["GET", "POST"],
)
def users(request):
    return html_response("<h1>Users</h1>")

Dynamic routes

Supported route converters:

Converter Example Python value
string /users/<string:name> str
int /users/<int:user_id> int
float /values/<float:value> float
path /files/<path:filename> str

Example:

@app.route("/users/<int:user_id>")
def user_profile(request, user_id):
    return html_response(
        f"<h1>User {user_id}</h1>"
    )

The path converter can contain / characters and must be the final route segment.

Request data

A view receives a backend Request object:

@app.route("/search")
def search(request):
    query = request.query.get(
        "query",
        [""],
    )[0]

    return html_response(
        f"<p>Search: {query}</p>"
    )

Frequently used request attributes and methods:

request.method
request.path
request.query_string
request.query
request.headers
request.body

request.get_header("Content-Type")
request.get_headers("Cookie")
request.cookies

JSON

@app.route("/api/users", methods=["POST"])
def create_user(request):
    data = request.get_json()

    return json_response(
        {
            "name": data["name"],
            "created": True,
        },
        status_code=201,
    )

URL-encoded forms

form = request.get_form()
username = form["username"][0]

Multipart forms and file uploads

form, files = request.get_multipart()

description = form["description"][0]
uploaded_file = files["document"][0]

uploaded_file.filename
uploaded_file.content_type
uploaded_file.body
uploaded_file.headers

Repeated fields and files are preserved as lists.

Response helpers

The public response helpers are:

content_response()
html_response()
text_response()
json_response()
file_response()
redirect_response()
empty_response()
set_cookie()
delete_cookie()

Examples:

return text_response(
    "Application is running"
)
return json_response(
    {"successful": True},
    status_code=200,
)
return redirect_response(
    "/login",
    status_code=302,
)
return empty_response(
    status_code=204
)

Character encoding

Text, HTML and JSON responses support configurable character encoding:

return html_response(
    "<h1>Hello</h1>",
    charset="utf-16",
)

Cookies

response = json_response(
    {"authenticated": True}
)

return set_cookie(
    response=response,
    key="session_id",
    value="abc123",
    max_age=3600,
    secure=True,
    http_only=True,
    same_site="Lax",
)

Delete a cookie:

response = text_response("Logged out")

return delete_cookie(
    response=response,
    key="session_id",
)

Static files

Static files are served from the static directory by default:

static/
├── css/
│   └── style.css
└── js/
    └── application.js

They are available under:

/static/css/style.css
/static/js/application.js

Custom configuration:

from basic_web_backend import (
    ApplicationConfig,
    WebApplication,
)


config = ApplicationConfig(
    static_folder="assets",
    static_url_path="/assets",
)

app = WebApplication(config=config)

Disable built-in static file serving:

config = ApplicationConfig(
    static_folder=None
)

The static file handler prevents paths from escaping the configured static directory.

Template engine

Templates are loaded from the templates directory by default.

Example template:

<h1>Hello, {{ username }}!</h1>

{% if active %}
<p>Your account is active.</p>
{% else %}
<p>Your account is inactive.</p>
{% endif %}

<ul>
{% for item in items %}
    <li>{{ item.name }}</li>
{% endfor %}
</ul>

Render the template from a view:

@app.route("/users/<string:username>")
def profile(request, username):
    return app.render_template(
        "profile.html",
        username=username,
        active=True,
        items=[
            {"name": "First"},
            {"name": "Second"},
        ],
    )

Template output is HTML-escaped by default.

Template configuration:

config = ApplicationConfig(
    template_folder="templates",
    template_encoding="utf-8",
    template_autoescape=True,
    template_cache=True,
    template_auto_reload=False,
)

HTTP errors

Views can raise public HTTP exceptions:

from basic_web_backend import (
    BadRequest,
    NotFound,
)


@app.route("/products/<int:product_id>")
def product(request, product_id):
    if product_id == 0:
        raise NotFound(
            path=request.path
        )

    if product_id < 0:
        raise BadRequest(
            "Product ID cannot be negative."
        )

    return html_response(
        f"<h1>Product {product_id}</h1>"
    )

Supported HTTP exceptions include:

  • BadRequest — 400
  • Unauthorized — 401
  • Forbidden — 403
  • NotFound — 404
  • MethodNotAllowed — 405
  • Conflict — 409
  • PayloadTooLarge — 413
  • UnsupportedMediaType — 415
  • UnprocessableContent — 422

Custom error handlers

@app.errorhandler(404)
def handle_not_found(error):
    return html_response(
        "<h1>Custom 404 page</h1>",
        status_code=404,
    )
@app.errorhandler(500)
def handle_internal_error(error):
    return html_response(
        "<h1>The service is unavailable</h1>",
        status_code=500,
    )

Unexpected exceptions produce a generic 500 response in production mode. Debug mode includes exception details in the response and must not be enabled in production.

Logging

Enable rotating application logs through ApplicationConfig:

config = ApplicationConfig(
    log_file="logs/backend.log",
    log_level="INFO",
    log_max_bytes=1_000_000,
    log_backup_count=5,
)

app = WebApplication(config=config)

When the active log reaches the configured size, older logs are stored as:

backend.log.1
backend.log.2
backend.log.3

Unexpected application errors and adapter failures are logged with their traceback. Normal 4xx client errors are not logged by the backend.

Do not store request bodies, passwords, authorization headers or cookie values in application logs.

Custom server adapters

A request adapter must provide:

class CustomRequestAdapter:
    def convert(self, server_request):
        return Request(
            method=server_request.method,
            path=server_request.path,
            query_string=server_request.query_string,
            headers=server_request.headers,
            body=server_request.body,
        )

It must return a backend Request object.

A response adapter must provide:

class CustomResponseAdapter:
    def convert(self, backend_response):
        return backend_response

Configure them with:

config = ApplicationConfig(
    request_adapter=CustomRequestAdapter(),
    response_adapter=CustomResponseAdapter(),
)

app = WebApplication(config=config)

A response adapter failure is logged and then propagated to the web server, because a failed response adapter cannot safely convert a backend-generated error response.

Version

import basic_web_backend

print(basic_web_backend.__version__)

License

This project is licensed under the MIT License.

Metadata

Release files for lema-basic-web-backend 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lema-basic-web-backend 0.1.0
File Size Uploaded
lema_basic_web_backend-0.1.0.tar.gz 43.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for lema-basic-web-backend 0.1.0
File Interpreter ABI Platform
lema_basic_web_backend-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 69.6 kB

Release files / lema_basic_web_backend-0.1.0.tar.gz

Download URL lema_basic_web_backend-0.1.0.tar.gz
Size 43.9 kB
Tags Source
SHA-256 checksum
How to use checksums
9f589f27fa5adfef4430cce6d80ad3a3456fe8dd25d700aee72b015532c0f21a
BLAKE2b-256 checksum
How to use checksums
b23842ccbd5d01d71178efe21d0b60c5bd976c0a2361ac4cc597ebe630600a00
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.

Transparency log

Release files / lema_basic_web_backend-0.1.0-py3-none-any.whl

Download URL lema_basic_web_backend-0.1.0-py3-none-any.whl
Size 25.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ea3561ad16ce16c0f1a747ec4038ac7f1afbf2d8aea31610c2c913315a132c97
BLAKE2b-256 checksum
How to use checksums
7547250b2945d66a0ff829fa143e00c0aa510a8d35dedf5d436a7ca435d56594
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page