Skip to main content

vs-server

HTTP server library for Python — built on FastAPI. Designed to feel like Spring MVC: annotate a controller class, define routes with method decorators, add guards for auth, and start the server with one line.


The Problem It Solves

Without vs-server, every FastAPI service repeats the same boilerplate:

# Without vs-server — repeated in every service
app = FastAPI(title="my-service", lifespan=lifespan)
app.add_middleware(CORSMiddleware, allow_origins=["*"], ...)
app.add_exception_handler(RequestValidationError, handler)
app.add_exception_handler(Exception, handler)

router = APIRouter(prefix="/users", tags=["Users"])

@router.get("", response_model=list[UserResponse])
async def list_users(): ...

@router.post("", status_code=201, response_model=UserResponse)
async def create_user(body: UserRequest): ...

app.include_router(router)
uvicorn.run(app, host="0.0.0.0", port=8080)

With vs-server:

# With vs-server — zero boilerplate
@controller("/users")
class UserController:

    @get("", response_model=list[UserResponse])
    async def list_users(self): ...

    @post("", status_code=201, response_model=UserResponse)
    async def create_user(self, body: UserRequest): ...

VsFastApiServer(config).add_controller(UserController()).run()

Installation

pip install vs-server[fastapi]

For future framework support:

pip install vs-server[flask]      # when available

How It All Fits Together

Application Startup
    └── VsLogManager.init(...)           # initialize logging
    └── VsDbSessionFactory(config)       # optional — if using vs-db
    └── VsCacheManager.init(...)         # optional — if using cache

Server
    └── VsFastApiServer(config)
            ├── reads name/version/host/port from config.ini
            ├── wires CORS, exception handlers, lifespan
            ├── auto-registers VsDbMiddleware if vs-db is initialized
            └── exposes /  and /health

Controllers
    └── @controller("/users", guards=[require_auth])
            └── @get, @post, @put, @delete, @patch on methods
            └── guards stack: controller guards run first, method guards append

Application Startup

from vs_common.config.vs_ini_config import VsIniConfig
from vs_common.log.vs_log_manager import VsLogManager
from vs_common.schema.vs_log_config import VsLogConfig
from vs_server.server.vs_fast_api_server import VsFastApiServer

if __name__ == "__main__":
    config = VsIniConfig("config.ini")

    VsLogManager.init(VsLogConfig(
        level=config.get("logging.level", "INFO"),
        file_path=config.get("logging.file_path", "logs/app.log"),
    ))

    server = VsFastApiServer(config)
    server.add_controller(UserController())
    server.add_controller(OrderController())
    server.run()

config.ini:

[server]
name        = my-service
version     = 1.0.0
description = My API
base_url    =
host        = 0.0.0.0
port        = 8080
reload      = false
workers     = 1
log_level   = info

[api]
cors_origins = https://myapp.com, https://admin.myapp.com

[logging]
level           = INFO
console_enabled = true
file_enabled    = true
file_path       = logs/app.log

@controller

Marks a class as a route controller. All routes defined inside are registered under the given base path.

from vs_server.decorator.vs_controller_decorator import controller, get, post, put, delete, patch

@controller("/users")
class UserController:
    ...

Register with the server:

server.add_controller(UserController())

The tag in Swagger/OpenAPI is auto-derived from the class name — UserController"User".


HTTP Method Decorators

Define routes directly on controller methods. All FastAPI route parameters are supported via **kwargs.

@controller("/users")
class UserController:

    @get("", response_model=list[UserResponse])
    async def list_users(self): ...

    @get("/{id}", response_model=UserResponse)
    async def get_user(self, id: int): ...

    @post("", status_code=201, response_model=UserResponse)
    async def create_user(self, body: UserRequest): ...

    @put("/{id}", response_model=UserResponse)
    async def update_user(self, id: int, body: UserRequest): ...

    @patch("/{id}", response_model=UserResponse)
    async def patch_user(self, id: int, body: UserPatchRequest): ...

    @delete("/{id}", status_code=204)
    async def delete_user(self, id: int): ...

Supported kwargs passed through to FastAPI:

Parameter Example Description
response_model response_model=UserResponse Pydantic model for response serialization
status_code status_code=201 HTTP status code for success
dependencies dependencies=[Depends(fn)] FastAPI dependencies
tags tags=["custom"] Override auto-generated Swagger tag
summary summary="List all users" Swagger summary
deprecated deprecated=True Mark route as deprecated in Swagger

Guards

Guards are callables that run before the route handler. They are the equivalent of Spring's @PreAuthorize or a scoped servlet filter.

Controller-level guards

Apply to every route in the controller:

from fastapi import Header
from vs_server.schema.exceptions import ForbiddenException

async def require_auth(authorization: str = Header(...)):
    if not is_valid_token(authorization):
        raise ForbiddenException("Invalid token")

@controller("/orders", guards=[require_auth])
class OrderController:

    @get("")
    async def list_orders(self): ...    # require_auth runs

    @post("")
    async def create_order(self): ...   # require_auth runs

Method-level guards

Method guards append on top of controller guards — they never replace them:

async def require_admin(authorization: str = Header(...)):
    if not is_admin(authorization):
        raise ForbiddenException("Admin access required")

@controller("/users", guards=[require_auth])
class UserController:

    @get("")
    async def list_users(self): ...                        # runs: require_auth

    @delete("/{id}", guards=[require_admin])
    async def delete_user(self, id: int): ...              # runs: require_auth → require_admin

Guards are any callable — auth, rate limiting, tenant validation, feature flags:

async def rate_limit(request: Request): ...
async def validate_tenant(x_tenant_id: str = Header(...)): ...
async def require_feature(x_feature: str = Header(...)): ...

@controller("/api", guards=[require_auth, rate_limit, validate_tenant])
class ApiController: ...

Exception Handling

Built-in exceptions

All exceptions produce a consistent response shape:

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "User not found",
    "details": {}
  }
}
Exception Status Code
NotFoundException 404 NOT_FOUND
ValidationException 400 VALIDATION_ERROR
UnauthorizedException 401 UNAUTHORIZED
ForbiddenException 403 FORBIDDEN
RateLimitException 429 RATE_LIMIT_EXCEEDED
TimeoutException 504 TIMEOUT_ERROR
ServiceUnavailableException 503 SERVICE_UNAVAILABLE
InternalServerException 500 INTERNAL_ERROR

Usage in a controller:

from vs_server.schema.exceptions import NotFoundException, ForbiddenException

@get("/{id}", response_model=UserResponse)
async def get_user(self, id: int):
    user = await self.service.get_by_id(id)
    if not user:
        raise NotFoundException(f"User {id} not found")
    return user

RateLimitException automatically adds a Retry-After header to the response.

Custom exception handlers

Override _register_exception_handlers() to add your own:

class MyServer(VsFastApiServer):

    def _register_exception_handlers(self, app) -> None:
        super()._register_exception_handlers(app)          # keep defaults
        app.add_exception_handler(PaymentException, payment_error_handler)
        app.add_exception_handler(InventoryException, inventory_error_handler)

Built-in Routes

Registered automatically on every server:

GET /

{
  "service": "my-service",
  "version": "1.0.0",
  "status": "running"
}

GET /health

{
  "status": "healthy",
  "name": "my-service",
  "version": "1.0.0",
  "uptime_seconds": 142.3,
  "cache": "healthy",
  "timestamp": "2026-07-19T10:00:00"
}

status is "healthy" when all components are up, "degraded" when cache is unreachable.


Lifecycle Hooks

Override startup() and shutdown() for custom logic:

class MyServer(VsFastApiServer):

    async def startup(self) -> None:
        await super().startup()
        await self.warm_up_cache()
        self._logger.info("Cache warmed up")

    async def shutdown(self) -> None:
        await self.flush_pending_jobs()
        await super().shutdown()

Custom Middleware

Override _register_middleware() to add your own middleware:

class MyServer(VsFastApiServer):

    def _register_middleware(self, app) -> None:
        super()._register_middleware(app)          # keep CORS
        app.add_middleware(MyRateLimitMiddleware)
        app.add_middleware(RequestIdMiddleware)

Using with vs-db

Initialize VsDbSessionFactory before creating the server — VsDbMiddleware is registered automatically:

from vs_common.config.vs_ini_config import VsIniConfig
from vs_common.log.vs_log_manager import VsLogManager
from vs_common.schema.vs_log_config import VsLogConfig
from vs_db.session.vs_db_session_factory import VsDbSessionFactory
from vs_server.server.vs_fast_api_server import VsFastApiServer

if __name__ == "__main__":
    config = VsIniConfig("config.ini")
    VsLogManager.init(VsLogConfig(...))
    VsDbSessionFactory(config)             # vs-db initialized → middleware auto-registered

    server = VsFastApiServer(config)
    server.add_controller(UserController())
    server.run()

No manual middleware registration needed.


Implementing Your Own Server

Extend VsServer to support any framework:

from vs_server.server.vs_server import VsServer
from vs_common.config.vs_base_config import VsBaseConfig

class VsFlaskServer(VsServer):

    def __init__(self, config: VsBaseConfig):
        try:
            from flask import Flask
        except ImportError:
            raise ImportError("Run: pip install vs-server[flask]")
        super().__init__(config)
        self._app = Flask(self.name)

    def add_router(self, router, prefix: str = "") -> None:
        self._app.register_blueprint(router, url_prefix=prefix)

    def add_controller(self, instance) -> None:
        # scan for _vs_route metadata and register with Flask
        ...

    def get_app(self):
        return self._app

    def run(self) -> None:
        self._app.run(
            host=self._config.get("server.host", "127.0.0.1"),
            port=self._config.get("server.port", 8000, int),
        )

Full Example

from pydantic import BaseModel
from fastapi import Header
from vs_server.decorator.vs_controller_decorator import controller, get, post, put, delete
from vs_server.schema.exceptions import NotFoundException, ForbiddenException


class UserRequest(BaseModel):
    name: str
    email: str

class UserResponse(BaseModel):
    id: int
    name: str
    email: str


async def require_auth(authorization: str = Header(...)):
    if authorization != "Bearer secret":
        raise ForbiddenException("Invalid token")

async def require_admin(authorization: str = Header(...)):
    if authorization != "Bearer admin":
        raise ForbiddenException("Admin access required")


@controller("/users", guards=[require_auth])
class UserController:

    def __init__(self):
        self.service = UserService()

    @get("", response_model=list[UserResponse])
    async def list_users(self):
        return await self.service.get_all()

    @get("/{id}", response_model=UserResponse)
    async def get_user(self, id: int):
        user = await self.service.get_by_id(id)
        if not user:
            raise NotFoundException(f"User {id} not found")
        return user

    @post("", status_code=201, response_model=UserResponse)
    async def create_user(self, body: UserRequest):
        return await self.service.create(body)

    @put("/{id}", response_model=UserResponse)
    async def update_user(self, id: int, body: UserRequest):
        return await self.service.update(id, body)

    @delete("/{id}", status_code=204, guards=[require_admin])
    async def delete_user(self, id: int):
        await self.service.delete(id)


if __name__ == "__main__":
    from vs_common.config.vs_ini_config import VsIniConfig
    from vs_common.log.vs_log_manager import VsLogManager
    from vs_common.schema.vs_log_config import VsLogConfig
    from vs_server.server.vs_fast_api_server import VsFastApiServer

    config = VsIniConfig("config.ini")
    VsLogManager.init(VsLogConfig(level="INFO"))

    server = VsFastApiServer(config)
    server.add_controller(UserController())
    server.run()

What vs-server eliminated:

  • No FastAPI() setup
  • No add_middleware() calls for CORS and exception handlers
  • No APIRouter() boilerplate
  • No include_router() calls
  • No uvicorn.run() wiring
  • Guards expressed as plain callables — no FastAPI Depends syntax in user code

Download files

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

Source Distribution

vs_server-0.1.0.tar.gz (23.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vs_server-0.1.0-py3-none-any.whl (18.7 kB view details)

Uploaded Python 3

File details

Details for the file vs_server-0.1.0.tar.gz.

File metadata

  • Download URL: vs_server-0.1.0.tar.gz
  • Upload date:
  • Size: 23.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for vs_server-0.1.0.tar.gz
Algorithm Hash digest
SHA256 df546669401a1408d7b206c81c90054e255acadcc11c9943ce0bdc48c8c8a049
MD5 3dcc4f785dca89087e5919b894720726
BLAKE2b-256 01dca965d54a81ec9a6a444b6a3afe9677261fee21d47fdc691688bf7295c6de

See more details on using hashes here.

File details

Details for the file vs_server-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: vs_server-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for vs_server-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0551ac970dce1426f3176d79d45debcddafef37d2ffcd20df854d3984dbe6261
MD5 84e4b951cf1e164f5e3e8e5109598718
BLAKE2b-256 5d3887e329e87efc5ef3b10e14789555a751e9dc21f54f3c5774e3b00f883d2a

See more details on using hashes here.

Supported by

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