Begin your journey by setting up your Serv application and getting familiar with the basic commands.
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.
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.
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
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.
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
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>
Middleware components allow you to process requests and responses globally or for specific routes, implement custom authentication, logging, and more.
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
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.
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
.
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.
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.
Consider joining the Serv community (details to be announced) to ask questions, share your projects, and contribute!