Welcome to Serv!

Your sleek, modern Python web framework journey starts now.

Get Started

Your Serv Adventure Guide

Getting Started with Serv

Begin your journey by setting up your Serv application and getting familiar with the basic commands.

Initialize Your Application

The first step is to initialize your Serv project. This creates a serv.config.yaml file in your current directory, which will hold your site's configuration, extension settings, and middleware definitions.

Run the following command in your project's root directory:

serv app init

You'll be prompted for a site name and description. These are optional but recommended for clarity.

Inspect Your Configuration

Once initialized, or any time you want to see how Serv interprets your configuration file (including resolved extension paths), use:

serv app details

This command displays a formatted view of your serv.config.yaml, making it easy to verify your setup.

Launch Your Serv App

To start the development server and see your application in action, run:

serv launch

By default, this serves your application (defined by serv.app:App if not overridden) on http://127.0.0.1:8000.

For a smoother development experience, especially when making frequent changes, use the --reload flag. This will automatically restart the server when code changes are detected:

serv launch --reload

You can also specify a different host or port:

serv launch --host 0.0.0.0 --port 8080 --reload

Working with Extensions

Extensions are the building blocks of your Serv application, allowing you to organize features and extend functionality. Serv provides CLI tools to help you create and manage them.

Scaffold a New Extension

To quickly create the basic file structure for a new extension (including a extension.yaml definition file and a main Python file), use:

serv create extension --name "My Extension Name"

You'll be prompted for the extension's author, description, and version. This information will populate the generated files. Extensions are typically created in a ./extensions/ directory.

A typical extension might define routes. For example, in your extension's main.py:

from serv import Route, Extension, ResponseBuilder

class MySampleRoute(Route):
    async def say_hello(self, request: GetRequest):
        return PlainTextResponse("Hello from MySampleRoute!")

class MyNewExtension(Extension):
    async def on_request_begin(self, router: Router = dependency()):
        router.add_route("/my-route", MySampleRoute)

Remember to update your extension's extension.yaml to point to your main extension class.


name: Sample Extension
entry: extensions.sample.sample:MyNewExtension
version: 0.1.0
author: Me
description: An awesome sample extension
Enable & Disable Extensions

To activate a extension and include it in your application's lifecycle, add it to your serv.config.yaml using:

serv extension enable <extension_name_or_entry_string>

You can use the simple name of the extension (e.g., my_new_extension if it's in your local ./extensions directory) or the full entry string (e.g., some_module.some_extension:SomeExtension).

To remove a extension from your configuration:

serv extension disable <extension_name_or_entry_string>

Using Middleware

Middleware components allow you to process requests and responses globally or for specific routes, implement custom authentication, logging, and more.

Scaffold New Middleware

Serv helps you get started with new middleware by creating a template:

serv create middleware --name "My Middleware"

You'll be prompted for the extension to add the middleware to and a brief description. This creates a new middleware file in the specified extension directory containing a class that inherits from serv.middleware.ServMiddleware.

The generated middleware class includes stubs for enter, leave, and on_error hooks, demonstrating how to use Bevy for dependency injection:

from serv import ServMiddleware, Request, ResponseBuilder
from bevy import dependency

class MyCustomMiddleware(ServMiddleware):
    async def enter(self, request: Request = dependency()):
        # Code to run before the request is handled
        # print(f"Entering middleware for {request.path}")
        pass

    async def leave(self, response: ResponseBuilder = dependency()):
        # Code to run after the request is handled, before the response is sent
        # print(f"Leaving middleware for {response.status_code}")
        pass

    async def on_error(self, exc: Exception, request: Request = dependency()):
        # Code to run if an exception occurs during request handling
        # print(f"Error in middleware: {exc} for {request.path}")
        pass
Configure Middleware

Once you've created middleware, you need to configure it in your extension's extension.yaml file. Add the middleware to the extension configuration:

name: My Extension
description: A extension with middleware
version: 0.1.0
author: Your Name
entry: my_extension.main:MyExtension

middleware:
  - entry: middleware_auth_check:auth_check_middleware
    config:
      timeout: 30

The middleware will be automatically loaded when the extension is enabled. You can also add middleware directly in your extension's code using app.add_middleware() during extension initialization.

Defining Routes

Routes are the heart of your web application, mapping URL paths to specific request handlers. In Serv, you define routes by creating classes that inherit from serv.routing.Route.

Basic Route Structure

A route class can contain multiple handler methods. Handler methods can have any descriptive name. Serv determines the specific handler to execute for an incoming request by matching the HTTP method (e.g., GET, POST) to a handler method whose first parameter is type-hinted with the corresponding Request subtype from serv.requests (e.g., GetRequest for GET, PostRequest for POST). These methods receive the specific request object and must return an instance of a Response subtype (e.g., TextResponse, JSONResponse from serv.response_types). These response objects are used to define the response body, status code, headers, etc.

from serv import Route
from serv.requests import GetRequest # Import specific request types
from serv.response_types import TextResponse # Or HTMLResponse, JSONResponse etc.

class GreeterRoute(Route):
    # This method handles GET requests because its first parameter is type-hinted as GetRequest.
    # The method name can be anything descriptive.
    async def show_greeting(self, request: GetRequest) -> TextResponse:
        name = request.query.get("name", "World")
        # Handlers should return a Response object.
        # TextResponse sets Content-Type: text/plain by default.
        # Status code and headers can be set via its constructor if needed.
        return TextResponse(f"Hello, {name}!")

Routes are typically added to your application via a extension's event handler (e.g., on_request_begin if adding to the main router, or a more specific event) by calling router.add_route("/path", YourRouteClass) on a Router instance obtained through dependency injection.

Annotated Response Types & Tuple Expansion

Serv supports enhanced response handling using typing.Annotated. If a route handler's return type is annotated (e.g., Annotated[dict, JSONResponse] or Annotated[tuple[str, dict], Jinja2Response]), Serv will use the annotation to wrap the handler's output.

For example, to return JSON:

from typing import Annotated
from serv import Route
from serv.response_types import JSONResponse

class DataRoute(Route):
    async def handle_get(self) -> Annotated[dict, JSONResponse]:
        return {"message": "This is a JSON response", "status": "ok"}

If your handler returns a tuple and uses an annotated response type that can accept multiple arguments (like Jinja2Response which might take a template name and context data), the tuple will be expanded as arguments to the response wrapper's constructor:

from typing import Annotated
from serv import Route
from serv.response_types import Jinja2Response

class ArticleRoute(Route):
    async def handle_get(self, article_id: str) -> Annotated[tuple[str, dict], Jinja2Response]:
        # Fetch article_data based on article_id
        article_data = {"title": "My Article", "content": "..."} 
        return "article_template.html", {"article": article_data}

This allows for cleaner and more expressive route handlers.

Further Resources

Read the Docs (Coming Soon!)

Consider joining the Serv community (details to be announced) to ask questions, share your projects, and contribute!