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
Dependssyntax 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df546669401a1408d7b206c81c90054e255acadcc11c9943ce0bdc48c8c8a049
|
|
| MD5 |
3dcc4f785dca89087e5919b894720726
|
|
| BLAKE2b-256 |
01dca965d54a81ec9a6a444b6a3afe9677261fee21d47fdc691688bf7295c6de
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0551ac970dce1426f3176d79d45debcddafef37d2ffcd20df854d3984dbe6261
|
|
| MD5 |
84e4b951cf1e164f5e3e8e5109598718
|
|
| BLAKE2b-256 |
5d3887e329e87efc5ef3b10e14789555a751e9dc21f54f3c5774e3b00f883d2a
|