servy
a down to earth webframework in nim
Summary
| Latest Version | Unknown |
|---|---|
| License | MIT |
| CI Status | Failing |
| Downloads | 0 |
| Last Indexed | 2026-07-21 05:24 |
Tags
Installation
nimble install servy
choosenim install servy
git clone https://github.com/xmonader/nim-servy
OS Compatibility
| Platform | Linux | macOS | Windows | FreeBSD | OpenBSD | NetBSD | Android | iOS | WASM | Embedded |
|---|---|---|---|---|---|---|---|---|---|---|
| servy | ✓ | ✓ | ✓ | - | - | - | - | - | - | - |
Source
| Repository | https://github.com/xmonader/nim-servy |
|---|---|
| Homepage | https://github.com/xmonader/nim-servy |
| Registry Source | nimble_official |
README
servy
A production-ready async web framework for Nim.
Installation
nimble install servy
Quickstart
import servy
when isMainModule:
var router = initRouter()
proc handleHello(req: Request, res: Response): Future[void] {.async.} =
res.json(%*{"message": "hello world!"})
router.addRoute("/hello", handleHello)
let opts = newServerOptions(port = 9000)
var s = initServy(opts, router)
s.run()
Routing
Basic routes
router.addRoute("/", handleIndex)
router.addRoute("/get", handleGet, HttpGet)
router.addRoute("/post", handlePost, HttpPost)
router.addRoute("/put", handlePut, HttpPut)
router.addRoute("/delete", handleDelete, HttpDelete)
URL parameters
proc handleUser(req: Request, res: Response): Future[void] {.async.} =
res.json(%*{"user": req.urlParams["user"]})
router.addRoute("/user/:user", handleUser)
router.addRoute("/multi/:first/:second", handleMultiParam)
Query parameters
proc handleQuery(req: Request, res: Response): Future[void] {.async.} =
res.json(%*{"params": req.queryParams})
Request: GET /query?page=1&limit=10 gives req.queryParams["page"] = "1".
Request/Response
JSON helpers
# Parse JSON body
proc handlePost(req: Request, res: Response): Future[void] {.async.} =
let body = req.parseJsonBody()
if body.kind != JNull:
res.json(%*{"received": body})
else:
res.abortWith("invalid json", Http400)
# Send JSON response
res.json(%*{"status": "ok"})
res.json(%*{"error": "not found"}, Http404)
Form data
proc handleForm(req: Request, res: Response): Future[void] {.async.} =
let username = req.formData.getValueOrNone("username")
res.json(%*{"user": username.get("")})
Cookies
proc handleCookies(req: Request, res: Response): Future[void] {.async.} =
let session = req.cookies.getOrDefault("session", "")
res.headers["Set-Cookie"] = "session=abc123; Path=/"
Response helpers
res.abortWith("forbidden", Http403)
res.redirectTo("/", Http302)
res.setHeader("X-Custom", "value")
res.addHeader("X-Another", "value")
Server options
let opts = newServerOptions(
address = "0.0.0.0",
port = 8080,
debug = true,
maxBodySize = 10 * 1024 * 1024, # 10MB body limit
requestTimeout = 30, # seconds
keepAlive = true,
keepAliveTimeout = 5
)
- Graceful shutdown: SIGINT/SIGTERM handled cleanly
- Body size limits: Returns 413 if request body exceeds
maxBodySize - Keep-alive: Connection reuse controlled by
keepAliveandkeepAliveTimeout - Error handling: Uncaught exceptions return 500 with JSON error body
Middleware
Middleware procs return Future[bool]. Return true to continue, false to short-circuit.
Production middleware
import servy/middleware_prod
# CORS
let cors = newCorsMiddleware(newCorsConfig())
# Rate limiting
let limiter = newRateLimiter(limit = 100, windowSeconds = 60)
let rateLimit = newRateLimitMiddleware(limiter)
# Request ID (adds X-Request-ID header)
let reqId = requestIdMiddleware()
# Security headers (CSP, X-Frame-Options, HSTS, etc.)
let security = newSecurityHeadersMiddleware()
# Request timing (adds X-Response-Time header)
let timing = requestTimingMiddleware()
# Gzip compression (adds Content-Encoding: gzip for large responses)
let gzip = gzipMiddleware()
# Body size limit
let bodyLimit = newRequestSizeLimitMiddleware(maxBodyBytes = 10 * 1024 * 1024)
# Structured logging
let logger = logMiddleware(level = Info)
# Apache-style request logging
let accessLog = requestLoggerMiddleware()
Use in server
var s = initServy(opts, router, @[
reqId,
security,
cors,
rateLimit,
logger,
timing,
gzip
])
Built-in middleware
import servy/middleware
# Logging
let logging = loggingMiddleware
# Trim trailing slashes
let trimSlash = trimTrailingSlash
# Static files
let serveStatic = newStaticMiddleware("/path/to/files", "/public")
# Basic auth
let users = {"admin": "secret"}.toTable
let auth = basicAuth(users)
Custom middleware
proc myMiddleware(req: Request, res: Response): Future[bool] {.async, closure, gcsafe.} =
let token = req.headers.getOrDefault("authorization", @[""])[0]
if token == "Bearer secret":
return true
else:
res.abortWith("unauthorized", Http401)
return false
router.addRoute("/protected", handleGet, HttpGet, @[myMiddleware])
Nim 2.x note: Local middleware procs must use {.async, closure, gcsafe.} pragmas.
Per-route vs global
# Per-route
router.addRoute("/admin", handleAdmin, HttpGet, @[auth])
# Global (applied to all routes)
var s = initServy(opts, router, @[reqId, security, logging])
WebSocket
Built-in WebSocket support (no external dependencies):
proc handleWS(req: Request, res: Response): Future[void] {.async.} =
var ws = await newServyWebSocket(req)
await ws.send("Welcome!")
while ws.readyState == Open:
let packet = await ws.receiveStrPacket()
await ws.send("echo: " & packet)
router.addRoute("/ws", handleWS, HttpGet)
Test with JavaScript:
ws = new WebSocket("ws://127.0.0.1:9000/ws")
ws.onmessage = (m) => console.log(m.data)
ws.send("hello")
Health check
router.addRoute("/health", healthCheckHandler)
# Returns {"status":"ok"} with 200
Module structure
servy/
types.nim # Core types, JSON helpers
router.nim # Route matching
parser.nim # HTTP request parsing
response.nim # Response formatting
middleware.nim # Basic middleware (logging, static, auth)
middleware_prod.nim # Production middleware (CORS, rate limit, security, etc.)
websocket.nim # Built-in WebSocket
server.nim # Server, graceful shutdown, timeouts
servy.nim # Facade
Running
nim c -r examples/hello.nim
# or
nimble run servy
Testing
make test # Run 24 tests
make build # Build all
Curl examples
curl localhost:9000/hello
curl localhost:9000/user/alice
curl -X POST -d '{"name":"john"}' -H "Content-Type: application/json" localhost:9000/api
curl -b "session=xyz" localhost:9000/cookies
curl -H "Authorization: Basic YWRtaW46c2VjcmV0" localhost:9000/admin
curl localhost:9000/health