FastNim

受 FastAPI 启发的 Nim 2 Web 框架 — 仅用标准库,极致性能,简洁语法

Nim 2.2+ 零第三方依赖 异步高性能 WebSocket 中间件 CORS 文件上传

介绍

FastNim 是一个受 Python FastAPI 启发的 Nim 2 Web 框架,设计目标是用 Nim 原生的语法风格提供类似 FastAPI 的开发体验,同时保持 Nim 语言本身的极致性能。

核心特性

  • 零第三方依赖 — 完全基于 Nim 标准库(asynchttpserverjsonasyncnetsha1 等)实现
  • 简洁的 DSL 语法 — 使用 Nim 模板宏实现 app.getapp.post 等路由注册,代码即文档
  • 路径参数 — 支持 /users/{id} 风格的路径参数自动提取
  • 查询参数 — 自动解析 URL 查询字符串
  • JSON / 表单 / 文件上传 — 内置 JSON、URL 编码表单、multipart 表单解析
  • 中间件 — 前置和后置中间件,支持请求拦截和响应修改
  • CORS 跨域 — 一行代码启用 CORS,支持预检请求
  • WebSocket — 完整实现 RFC 6455,支持文本/二进制消息、Ping/Pong
  • 静态文件 — 挂载目录提供静态文件服务,自动 MIME 类型检测
  • 异常处理 — 自定义 HTTP 异常处理器
  • 生命周期钩子 — 启动和关闭时的回调函数
  • Cookie 操作 — 设置、获取、删除 Cookie

与 FastAPI 的对比

特性FastAPIFastNim
语言PythonNim 2
异步asyncioasyncdispatch
路由注册@app.get 装饰器app.get 模板
路径参数{id}{id}
依赖注入Depends()中间件 + ctx.state
自动文档Swagger / ReDoc不支持(按需)
第三方依赖Starlette, Pydantic 等无(纯标准库)
性能高(ASGI)极高(编译为原生代码)

安装与配置

环境要求

  • Nim 2.2.0 或更高版本
  • 支持 Linux、macOS、Windows

获取 FastNim

FastNim 是一个纯 Nim 源码库,无需安装二进制包。将源码放入项目即可使用。

方式一:直接复制源码

src/fastnim.nimsrc/fastnim/ 目录复制到你的项目中。

方式二:通过 nimble 安装(如果有 nimble 包)

# 在项目目录下
nimble install fastnim

项目配置

在你的项目中创建 nim.cfg 文件,添加以下配置:

# nim.cfg
--threads:off
--mm:orc
--path:src
关于 --threads:off

Nim 的异步 HTTP 服务器在单线程模式下运行效率最高。--threads:off 避免了 GC 安全检查的限制,同时 --mm:orc 提供了更好的内存管理。

目录结构

myapp/
├── nim.cfg          # 编译配置
├── main.nim         # 主程序
└── src/
    ├── fastnim.nim   # 框架入口
    └── fastnim/      # 框架子模块
        ├── types.nim
        ├── router.nim
        ├── context.nim
        ├── websocket.nim
        └── multipart.nim

快速开始

以下是一个最简单的 FastNim 应用,只需 5 行代码即可启动一个 HTTP 服务器:

import fastnim

let app = newApp()

app.get "/":
  ctx.json(%*{"message": "Hello, World!"})

app.run()

编译并运行:

nim c -r main.nim
# FastNim server running on http://0.0.0.0:8080

测试:

curl http://localhost:8080/
# {"message":"Hello, World!"}

代码解析

  • import fastnim — 导入框架,自动导出所有必要的类型和函数
  • newApp() — 创建应用实例,默认监听 0.0.0.0:8080
  • app.get "/" — 注册 GET 路由,ctx 变量在处理体内自动可用
  • ctx.json(...) — 返回 JSON 响应,%*{} 是 Nim 的 JSON 字面量语法
  • app.run() — 启动服务器(阻塞调用)
提示

ctx 是框架自动注入的上下文变量,无需手动声明。它封装了请求和响应的所有信息。

路由

FastNim 支持所有标准 HTTP 方法。每个路由由 HTTP 方法、路径模式和处理器组成。

HTTP 方法

框架为每种 HTTP 方法提供了对应的注册模板:

let app = newApp()

# GET 请求
app.get "/":
  ctx.json(%*{"method": "GET"})

# POST 请求
app.post "/create":
  ctx.json(%*{"method": "POST"}, Http201)

# PUT 请求(完整更新)
app.put "/update/{id}":
  ctx.json(%*{"method": "PUT", "id": ctx.pathParam("id")})

# PATCH 请求(部分更新)
app.patch "/patch/{id}":
  ctx.json(%*{"method": "PATCH"})

# DELETE 请求
app.delete "/delete/{id}":
  ctx.json(%*{"method": "DELETE"})

# HEAD 请求(只返回头,不返回体)
app.head "/health":
  ctx.setHeader("X-Status", "healthy")
  ctx.noContent()

# OPTIONS 请求
app.options "/api":
  ctx.noContent()

路由匹配规则

  • 路径匹配区分大小写
  • 尾部斜杠自动忽略(/users/users/ 等价)
  • 静态段优先于参数段匹配
  • 未匹配到路由时返回 404

指定监听端口和地址

# 默认 0.0.0.0:8080
app.run()

# 自定义端口
app.run(3000)

# 自定义地址和端口
app.run(3000, "127.0.0.1")

路径参数

路径参数使用 {参数名} 语法在路由模式中声明,框架会自动从 URL 中提取并填充到 ctx.pathParams

# 单个路径参数
app.get "/users/{id}":
  let id = ctx.pathParam("id")
  ctx.json(%*{"user_id": id})

# 多个路径参数
app.get "/users/{userId}/posts/{postId}":
  let userId = ctx.pathParam("userId")
  let postId = ctx.pathParam("postId")
  ctx.json(%*{"user": userId, "post": postId})

# 转为整数
app.get "/items/{id}":
  let id = ctx.pathParamInt("id")
  ctx.json(%*{"id": id, "type": "number"})

# 带默认值
app.get "/page/{num}":
  let page = ctx.pathParamIntOrDefault("num", 1)
  ctx.json(%*{"page": page})

路径参数 API

方法返回类型说明
ctx.pathParam(name)string获取路径参数值
ctx.pathParamOrDefault(name, default)string获取路径参数,不存在时返回默认值
ctx.pathParamInt(name)int获取路径参数并转为 int
ctx.pathParamIntOrDefault(name, default)int获取路径参数并转为 int,带默认值

查询参数

查询参数是 URL 中 ? 之后的部分,格式为 key1=value1&key2=value2。框架自动解析并存储在 ctx.queryParams 中。

# 基本用法
# GET /search?q=nim&limit=10
app.get "/search":
  let q = ctx.queryParam("q")
  let limit = ctx.queryParamOrDefault("limit", "10")
  ctx.json(%*{"query": q, "limit": limit})

# 转为整数
# GET /products?page=2&page_size=20
app.get "/products":
  let page = ctx.queryParamIntOrDefault("page", 1)
  let pageSize = ctx.queryParamIntOrDefault("page_size", 10)
  ctx.json(%*{"page": page, "page_size": pageSize})

# 布尔值
# GET /api?verbose=true
app.get "/api":
  let verbose = ctx.queryParamBool("verbose")
  ctx.json(%*{"verbose": verbose})

# 检查参数是否存在
app.get "/filter":
  if ctx.hasQueryParam("category"):
    let category = ctx.queryParam("category")
    ctx.json(%*{"category": category})
  else:
    ctx.json(%*{"error": "缺少 category 参数"}, Http400)

# 获取所有查询参数
app.get "/debug":
  var params = newJObject()
  for key, val in ctx.allQueryParams():
    params[key] = %val
  ctx.json(params)

查询参数 API

方法返回类型说明
ctx.queryParam(name)string获取查询参数值
ctx.queryParamOrDefault(name, default)string获取查询参数,带默认值
ctx.queryParamInt(name)int获取查询参数并转为 int
ctx.queryParamIntOrDefault(name, default)int获取查询参数并转为 int,带默认值
ctx.queryParamBool(name)bool获取查询参数并转为 bool
ctx.hasQueryParam(name)bool检查查询参数是否存在
ctx.allQueryParams()Table获取所有查询参数

请求体处理

FastNim 支持三种请求体格式:JSON、URL 编码表单和 multipart 表单(含文件上传)。

JSON 请求体

使用 ctx.jsonBody() 将请求体解析为 JsonNode

# POST /users  Content-Type: application/json
# {"name": "Alice", "age": 30}
app.post "/users":
  let body = ctx.jsonBody()
  let name = body["name"].getStr()
  let age = body["age"].getInt()
  ctx.json(%*{"created": {"name": name, "age": age}}, Http201)

# 安全解析(解析失败返回默认值)
app.post "/data":
  let body = ctx.jsonBodyOrDefault(%*{"error": "invalid"})
  ctx.json(body)

# 检查是否为 JSON 请求
app.post "/api":
  if ctx.isJson():
    let data = ctx.jsonBody()
    ctx.json(%*{"received": data})
  else:
    ctx.json(%*{"error": "请发送 JSON"}, Http400)

# 获取原始请求体
app.post "/raw":
  let raw = ctx.body()
  ctx.text("Received: " & raw)

表单数据

解析 application/x-www-form-urlencoded 格式的表单数据:

# POST /login  Content-Type: application/x-www-form-urlencoded
# username=admin&password=123456
app.post "/login":
  let form = ctx.formBody()
  let username = form.getOrDefault("username")
  let password = form.getOrDefault("password")
  if username == "admin" and password == "123456":
    ctx.json(%*{"message": "登录成功"})
  else:
    ctx.json(%*{"error": "用户名或密码错误"}, Http401)

# 遍历所有表单字段
app.post "/form":
  let form = ctx.formBody()
  var result = newJObject()
  for key, val in form:
    result[key] = %val
  ctx.json(%*{"received": result})

文件上传

使用 ctx.multipartBody() 解析 multipart/form-data 格式的请求体,支持文件上传:

# POST /upload  Content-Type: multipart/form-data
app.post "/upload":
  if ctx.isMultipart():
    let form = ctx.multipartBody()

    # 获取普通字段
    let description = form.getField("description")

    # 获取上传的文件
    if form.hasFile("file"):
      let file = form.files["file"]
      ctx.json(%*{
        "filename": file.filename,
        "contentType": file.contentType,
        "size": file.data.len,
        "description": description
      })
    else:
      ctx.json(%*{"error": "未找到文件"}, Http400)
  else:
    ctx.json(%*{"error": "请使用 multipart/form-data"}, Http400)

# 多文件上传
app.post "/upload-multi":
  let form = ctx.multipartBody()
  var filesInfo = newJObject()
  for name, file in form.files:
    filesInfo[name] = %*{
      "filename": file.filename,
      "size": file.data.len
    }
  ctx.json(%*{"files": filesInfo})
UploadedFile 结构

UploadedFile 包含三个字段:filename(原始文件名)、contentType(MIME 类型)、data(文件内容字符串)。

请求头

FastNim 提供了完整的请求头访问接口:

# 获取单个请求头
app.get "/api":
  let auth = ctx.getHeader("Authorization")
  let ua = ctx.userAgent()
  let ct = ctx.contentType()
  ctx.json(%*{"auth": auth, "ua": ua, "ct": ct})

# 带默认值
app.get "/safe":
  let accept = ctx.getHeaderOrDefault("Accept", "*/*")
  ctx.text(accept)

# 检查请求头是否存在
app.get "/check":
  if ctx.hasHeader("X-API-Key"):
    ctx.json(%*{"status": "authorized"})
  else:
    ctx.json(%*{"error": "Missing API Key"}, Http401)

# 获取所有请求头
app.get "/headers":
  var headers = newJObject()
  for key, val in ctx.allHeaders():
    headers[key] = %val
  ctx.json(headers)

# Bearer Token 提取
app.get "/profile":
  let token = ctx.bearerToken()
  if token.len == 0:
    ctx.json(%*{"error": "未授权"}, Http401)
    return
  ctx.json(%*{"token": token})

便捷属性

方法说明
ctx.getHeader(name)获取请求头值
ctx.getHeaderOrDefault(name, default)获取请求头,带默认值
ctx.hasHeader(name)检查请求头是否存在
ctx.allHeaders()获取所有请求头
ctx.contentType()获取 Content-Type
ctx.contentLength()获取 Content-Length
ctx.userAgent()获取 User-Agent
ctx.authorization()获取 Authorization 头
ctx.bearerToken()提取 Bearer Token

Cookie 操作

FastNim 支持完整的 Cookie 读写操作:

# 设置 Cookie
app.post "/login":
  let body = ctx.jsonBody()
  if body["username"].getStr() == "admin":
    ctx.setCookie("session", "abc123token",
                  path = "/", httpOnly = true)
    ctx.json(%*{"message": "登录成功"})

# 获取 Cookie
app.get "/profile":
  let session = ctx.getCookie("session")
  if session == "abc123token":
    ctx.json(%*{"user": "admin"})
  else:
    ctx.json(%*{"error": "未登录"}, Http401)

# 带默认值获取
app.get "/theme":
  let theme = ctx.getCookieOrDefault("theme", "light")
  ctx.json(%*{"theme": theme})

# 检查 Cookie 是否存在
app.get "/check-session":
  if ctx.hasCookie("session"):
    ctx.json(%*{"status": "logged in"})
  else:
    ctx.json(%*{"status": "guest"})

# 删除 Cookie(退出登录)
app.post "/logout":
  ctx.deleteCookie("session")
  ctx.json(%*{"message": "已退出"})

# 设置带属性的 Cookie
app.get "/set-preferences":
  ctx.setCookie("lang", "zh-CN",
                domain = ".example.com",
                path = "/",
                maxAge = 86400,      # 1 天
                secure = true,
                httpOnly = true)
  ctx.json(%*{"message": "偏好已保存"})

响应类型

FastNim 提供了多种响应类型,满足不同的 API 需求:

JSON 响应

app.get "/json":
  ctx.json(%*{"message": "Hello"})

# 自定义状态码
app.post "/create":
  ctx.json(%*{"id": 1}, Http201)

# 状态码在前(两种写法等价)
app.get "/error":
  ctx.json(Http404, %*{"error": "Not Found"})

HTML 响应

app.get "/page":
  ctx.html("<h1>Hello, World!</h1><p>Welcome to FastNim</p>")

纯文本响应

app.get "/text":
  ctx.text("Plain text response")

重定向

app.get "/old":
  ctx.redirect("/new")         # 302 临时重定向

app.get "/moved":
  ctx.redirect("/new", Http301)  # 301 永久重定向

无内容响应 (204)

app.delete "/items/{id}":
  # 删除逻辑...
  ctx.noContent()  # 204 No Content

原始响应

app.get "/xml":
  ctx.raw("<?xml version=\"1.0\"?><root>data</root>",
          "application/xml")

自定义响应头和状态码

app.get "/custom":
  ctx.status(Http200)
  ctx.setHeader("X-Custom-Header", "value")
  ctx.setHeader("Cache-Control", "no-cache")
  ctx.json(%*{"msg": "custom response"})

错误响应

app.get "/error":
  ctx.errorResponse(Http400, "参数错误")
  # 返回: {"error":"参数错误","status":400}

中间件

中间件是在请求处理前/后执行的函数,用于实现日志、认证、CORS 等横切关注点。

前置中间件

前置中间件在路由处理器之前执行。如果中间件设置了响应(ctx.sent = true),路由处理器不会被调用。

# 日志中间件
app.middleware:
  echo ctx.httpMethod(), " ", ctx.path(), " from ", ctx.clientIP()

# 认证中间件
app.middleware:
  let path = ctx.path()
  if path.startsWith("/api/") and path != "/api/login":
    let token = ctx.bearerToken()
    if token.len == 0:
      ctx.json(%*{"error": "未授权"}, Http401)
      return  # 短路,不执行后续中间件和路由处理器

# 添加自定义响应头
app.middleware:
  ctx.setHeader("X-Powered-By", "FastNim")
  ctx.setHeader("X-Request-ID", $getTime().toUnix())

# 使用 ctx.state 在中间件间传递数据
app.middleware:
  ctx.state["user_id"] = "12345"

后置中间件

后置中间件在路由处理器之后执行,可以修改已设置的响应:

app.afterMiddleware:
  # 记录响应状态
  echo "Response: ", ctx.response.statusCode.int

  # 添加响应时间头
  ctx.setHeader("X-Response-Time", "5ms")
注意

前置中间件中使用 return 可以短路请求链。当 ctx.senttrue 时,后续中间件和路由处理器都不会执行。

错误处理

抛出 HTTP 异常

使用 raiseHttpError 在处理器中抛出 HTTP 异常:

app.get "/users/{id}":
  let id = ctx.pathParamInt("id")
  if id > 100:
    raiseHttpError(Http404, "用户不存在")
  if id == 0:
    raiseHttpError(Http400, "ID 不能为 0")
  ctx.json(%*{"id": id})

app.post "/users":
  let body = ctx.jsonBody()
  if not body.hasKey("name"):
    raiseHttpError(Http400, "缺少 name 字段")
  ctx.json(%*{"created": true}, Http201)

自定义异常处理器

使用 app.exceptionHandler 注册自定义异常处理器,覆盖默认的错误响应格式:

# 404 处理器
app.exceptionHandler(404):
  ctx.json(%*{
    "error": "资源不存在",
    "path": ctx.path(),
    "method": $ctx.httpMethod()
  }, Http404)

# 500 处理器
app.exceptionHandler(500):
  ctx.json(%*{
    "error": "服务器内部错误",
    "timestamp": $getTime().toUnix()
  }, Http500)

# 401 处理器
app.exceptionHandler(401):
  ctx.setHeader("WWW-Authenticate", "Bearer")
  ctx.json(%*{"error": "请先登录"}, Http401)

异常处理流程

  1. 路由处理器抛出 HttpError
  2. 框架检查是否有对应状态码的自定义异常处理器
  3. 如果有,调用自定义处理器;如果没有,返回默认错误 JSON
  4. 对于未捕获的 CatchableError,按 500 处理

CORS 跨域

一行代码即可启用 CORS 支持:

# 基本启用(允许所有源)
app.enableCors()

# 自定义配置
app.enableCors(
  allowOrigins = @["https://example.com", "https://app.example.com"],
  allowMethods = @["GET", "POST"],
  allowHeaders = @["Content-Type", "Authorization"],
  allowCredentials = true,
  maxAge = 3600,
  exposeHeaders = @["X-Custom-Header"]
)

# 使用 CorsConfig 对象
app.enableCors(CorsConfig(
  allowOrigins: @["*"],
  allowMethods: @["GET", "POST", "PUT", "DELETE"],
  allowHeaders: @["*"],
  allowCredentials: false,
  maxAge: 600,
  exposeHeaders: @[]
))
CORS 预检请求

启用 CORS 后,框架会自动处理 OPTIONS 预检请求,返回 204 No Content 和相应的 CORS 头。你无需为 CORS 单独注册 OPTIONS 路由。

文件下载与传输

FastNim 提供完整的文件下载和传输功能,包括内联显示、强制下载、动态文件生成、分块传输和 Range 请求支持。

发送文件(内联显示)

ctx.sendFile() 发送文件,浏览器可能直接内联显示(如图片、PDF):

app.get "/images/{name}":
  ctx.sendFile("./uploads/" & ctx.pathParam("name"))

强制下载(附件)

ctx.download() 设置 Content-Disposition: attachment,强制浏览器下载文件:

app.get "/download/{name}":
  ctx.download("./uploads/" & ctx.pathParam("name"))

# 自定义下载文件名
app.get "/report":
  ctx.download("./data/report.pdf", fileName = "月度报告.pdf")

动态生成下载文件

ctx.downloadBytes() 发送字节数据作为下载文件,适用于动态生成 CSV、Excel 等:

app.get "/export/csv":
  let csv = "name,age\nAlice,30\nBob,25\n"
  ctx.downloadBytes(csv, "users.csv", "text/csv")

分块传输大文件

ctx.sendFileChunked() 分块读取文件,避免一次性加载到内存:

app.get "/video/{name}":
  ctx.sendFileChunked("./videos/" & ctx.pathParam("name"), chunkSize = 65536)

Range 请求(视频流 / 断点续传)

ctx.serveFileRange() 自动处理 Range 请求头,返回 206 Partial Content,支持视频流和断点续传:

app.get "/stream/{name}":
  ctx.serveFileRange("./media/" & ctx.pathParam("name"))

客户端发送 Range: bytes=0-1023 请求头时,服务器返回指定范围的数据,并设置 Content-RangeAccept-Ranges: bytes 响应头。

方法用途典型场景
sendFile内联发送文件图片预览、PDF 在线查看
download强制下载文件下载按钮
downloadBytes动态内容下载CSV/Excel 导出
sendFileChunked分块传输大文件传输
serveFileRangeRange 请求视频流、断点续传
HEAD 请求自动处理

框架自动将 HEAD 请求映射到对应的 GET 路由,返回相同的响应头但不返回 body。这意味着 curl -I 可以正常获取文件信息(Content-Type、Content-Length 等)而无需下载完整文件。

静态文件

使用 app.mount 挂载静态文件目录:

# 挂载 ./public 目录到 /static 路径
app.mount("/static", "./public")

# 访问 http://localhost:8080/static/index.html
# 会返回 ./public/index.html 文件

# 多个挂载点
app.mount("/css", "./assets/css")
app.mount("/js", "./assets/js")
app.mount("/images", "./assets/images")
安全提示

框架自动防止目录遍历攻击(../ 路径会被拒绝)。访问目录根路径时会自动尝试返回 index.html

框架会根据文件扩展名自动设置 Content-Type

扩展名Content-Type
.htmltext/html
.csstext/css
.jsapplication/javascript
.jsonapplication/json
.pngimage/png
.jpgimage/jpeg
.svgimage/svg+xml
.pdfapplication/pdf

WebSocket

FastNim 实现了完整的 WebSocket 协议(RFC 6455),支持文本/二进制消息、Ping/Pong 和分片消息。

基本用法

# 注册 WebSocket 路由
app.websocket "/ws":
  # ws 变量自动可用,代表 WebSocket 连接
  # ctx 变量也可用,代表请求上下文

  # 发送欢迎消息
  await ws.sendText("连接成功!")

  # 消息循环
  while not ws.isClosed:
    let msg = await ws.recvMessage()

    case msg.kind:
    of wsmText:
      # 回显文本消息
      await ws.sendText("Echo: " & msg.text)

    of wsmBinary:
      # 回显二进制消息
      await ws.sendBinary(msg.data)

    of wsmClose:
      # 客户端关闭连接
      break

    else:
      discard

发送 JSON 消息

app.websocket "/ws/json":
  await ws.sendJson(%*{"type": "welcome", "message": "Hello!"})

  while not ws.isClosed:
    let msg = await ws.recvMessage()
    if msg.kind == wsmText:
      let data = parseJson(msg.text)
      await ws.sendJson(%*{"type": "response", "data": data})

WebSocket API

方法说明
ws.sendText(text)发送文本消息
ws.sendBinary(data)发送二进制消息
ws.sendJson(data)发送 JSON 消息
ws.recvMessage()接收消息(自动处理 Ping/Pong)
ws.ping(data)发送 Ping 帧
ws.pong(data)发送 Pong 帧
ws.close(code, reason)关闭连接
ws.isClosed()检查连接是否已关闭
WebSocketMessage 类型

recvMessage() 返回 WebSocketMessage 对象,kind 字段表示消息类型:wsmText(文本)、wsmBinary(二进制)、wsmClose(关闭)、wsmPingwsmPong。Ping 消息会自动回复 Pong,无需手动处理。

生命周期事件

FastNim 提供了启动和关闭时的生命周期钩子:

# 启动时执行(服务器开始监听前)
app.onStartup:
  echo "初始化数据库连接..."
  # 这里可以执行数据库连接、加载配置等初始化操作

# 关闭时执行(Ctrl+C 触发)
app.onShutdown:
  echo "关闭数据库连接..."
  # 这里可以执行资源清理操作

# 多个钩子按注册顺序执行
app.onStartup:
  echo "加载缓存..."

app.onStartup:
  echo "启动后台任务..."
优雅关闭

当用户按下 Ctrl+C 时,框架会先执行所有 onShutdown 钩子,然后退出程序。这确保了资源的正确释放。

后台任务

后台任务是在服务器启动后异步执行的任务,不会阻塞请求处理:

# 定期清理过期 session
proc cleanupTask(): Future[void] {.async.} =
  while true:
    await sleepAsync(60000)  # 每分钟执行一次
    echo "清理过期 session..."
    # 清理逻辑...

app.onStartup:
  app.addBackgroundTask(cleanupTask)

# 发送邮件等异步操作
proc sendEmail(to, subject, body: string): Future[void] {.async.} =
  await sleepAsync(1000)  # 模拟发送
  echo "邮件已发送到: ", to

app.post "/register":
  let body = ctx.jsonBody()
  let email = body["email"].getStr()
  # 异步发送邮件,不阻塞响应
  asyncCheck sendEmail(email, "欢迎注册", "...")
  ctx.json(%*{"message": "注册成功"}, Http201)

API 文档(Swagger/OpenAPI)

FastNim 实现了 FastAPI 风格的自动 API 文档生成功能,包括 Swagger UI、ReDoc 和 OpenAPI JSON Schema。文档默认启用,无需额外配置。

访问文档

路径说明
/docsSwagger UI 交互式文档(可在线测试 API)
/redocReDoc 文档(只读,排版更美观)
/openapi.jsonOpenAPI 3.0 JSON Schema(可导入其他工具)

配置文档

# 默认启用,可自定义标题和版本
app.enableDocs("我的 API", "2.0.0", "这是一个示例 API 服务")

# 禁用文档
app.disableDocs()

带文档的路由注册

使用 docGetdocPost 等模板注册路由时,可同时提供摘要和标签:

# 带文档的 GET 路由
app.docGet "/users", "获取用户列表", ["users"]:
  ctx.json(%*{"users": []})

# 带文档的 POST 路由
app.docPost "/users", "创建用户", ["users"]:
  let body = ctx.jsonBody()
  ctx.json(%*{"id": 1, "name": body["name"].getStr()}, Http201)

# 带文档的 DELETE 路由
app.docDelete "/users/{id}", "删除用户", ["users"]:
  ctx.noContent()

# 普通路由也可用 describeLast 补充文档
app.get "/health":
  ctx.json(%*{"status": "ok"})
app.describeLast(summary = "健康检查", tags = ["system"])

请求体文档(文件上传 / JSON Body)

为了让 Swagger UI 显示文件上传控件或 JSON 输入框,需要为路由添加请求体文档。框架提供三个便捷方法:

# 文件上传 — Swagger UI 会显示 "Choose File" 按钮
app.docPost "/upload", "文件上传", ["files"]:
  let form = ctx.multipartBody()
  let file = form.files["file"]
  ctx.json(%*{"filename": file.filename, "size": file.data.len})
app.describeFileUpload("file", @[
  DocField(name: "description", kind: dfkString, required: false, description: "文件描述")
], "上传文件,支持任意类型")

# JSON Body — Swagger UI 会显示 JSON 输入框
app.docPost "/users", "创建用户", ["users"]:
  let body = ctx.jsonBody()
  ctx.json(%*{"id": 1, "name": body["name"].getStr()}, Http201)
app.describeJsonBody(@[
  DocField(name: "name", kind: dfkString, required: true, description: "用户名"),
  DocField(name: "email", kind: dfkString, required: true, description: "邮箱"),
  DocField(name: "age", kind: dfkInteger, required: false, description: "年龄")
])

# 通用方法 — 支持所有请求体类型
app.describeBody(dbkFormUrlencoded, @[
  DocField(name: "username", kind: dfkString, required: true),
  DocField(name: "password", kind: dfkString, required: true)
])

字段类型(DocFieldKind):

类型说明OpenAPI 映射
dfkString字符串type: string
dfkInteger整数type: integer
dfkNumber数字type: number
dfkBoolean布尔type: boolean
dfkFile文件type: string, format: binary

请求体类型(DocBodyKind):

类型Content-Type说明
dbkJsonapplication/jsonJSON 请求体
dbkMultipartmultipart/form-data文件上传
dbkFormUrlencodedapplication/x-www-form-urlencoded表单提交
文件上传要点

使用 describeFileUpload 后,Swagger UI 的 /docs 页面会显示 Choose File 按钮,可以直接在浏览器中选择文件并测试上传。这是通过 OpenAPI 的 format: binary 字段类型实现的。

OpenAPI 自动生成

框架自动从路由注册信息中提取路径、方法、路径参数,生成 OpenAPI 3.0 JSON。使用 docGet/docPost 等模板注册的路由会包含摘要和标签信息。Swagger UI 和 ReDoc 通过 CDN 加载,无需安装额外依赖。

API 参考

本节列出 FastNim 所有公开 API,按模块分类整理。

App 类型与构造

API签名说明
newAppnewApp(address = "0.0.0.0", port = 8080): App创建应用实例,可指定监听地址和端口
app.runapp.run(port = 0, address = "")启动服务器(阻塞),port/address 为 0/空时使用构造时的值
app.runAsyncapp.runAsync(port = 0, address = "")异步启动服务器,返回 Future
app.stateTable[string, string]全局应用状态,可在任意位置读写
app.routesseq[Route]已注册的路由列表

路由注册模板

模板说明
app.get(path, body)注册 GET 路由
app.post(path, body)注册 POST 路由
app.put(path, body)注册 PUT 路由
app.patch(path, body)注册 PATCH 路由
app.delete(path, body)注册 DELETE 路由
app.head(path, body)注册 HEAD 路由
app.options(path, body)注册 OPTIONS 路由
app.websocket(path, body)注册 WebSocket 路由,body 中可用 wsctx
路由体变量

在路由处理体中,ctx: Context 变量自动可用。在 WebSocket 路由体中,ws: WebSocketctx: Context 均自动可用。路径参数使用 {name} 语法声明,如 /users/{id}

Context — 路径与查询参数

方法签名说明
pathParamctx.pathParam(name): string获取路径参数
pathParamOrDefaultctx.pathParamOrDefault(name, default): string获取路径参数,带默认值
pathParamIntctx.pathParamInt(name): int获取路径参数并转为 int
pathParamIntOrDefaultctx.pathParamIntOrDefault(name, default): int获取路径参数并转为 int,带默认值
queryParamctx.queryParam(name): string获取查询参数
queryParamOrDefaultctx.queryParamOrDefault(name, default): string获取查询参数,带默认值
queryParamIntctx.queryParamInt(name): int获取查询参数并转为 int
queryParamIntOrDefaultctx.queryParamIntOrDefault(name, default): int获取查询参数并转为 int,带默认值
queryParamBoolctx.queryParamBool(name): bool获取查询参数并转为 bool("true"/"1"/"yes")
hasQueryParamctx.hasQueryParam(name): bool检查查询参数是否存在
allQueryParamsctx.allQueryParams(): Table[string, string]获取所有查询参数

Context — 请求头

方法签名说明
getHeaderctx.getHeader(name): string获取请求头
getHeaderOrDefaultctx.getHeaderOrDefault(name, default): string获取请求头,带默认值
hasHeaderctx.hasHeader(name): bool检查请求头是否存在
allHeadersctx.allHeaders(): HttpHeaders获取所有请求头
contentTypectx.contentType(): string获取 Content-Type
contentLengthctx.contentLength(): int获取 Content-Length
userAgentctx.userAgent(): string获取 User-Agent
authorizationctx.authorization(): string获取 Authorization 头
bearerTokenctx.bearerToken(): string提取 Bearer Token

Context — 请求体

方法签名说明
bodyctx.body(): string获取原始请求体
jsonBodyctx.jsonBody(): JsonNode解析 JSON 请求体,失败抛 400
jsonBodyOrDefaultctx.jsonBodyOrDefault(default): JsonNode解析 JSON 请求体,失败返回默认值
formBodyctx.formBody(): Table[string, string]解析 URL 编码表单
multipartBodyctx.multipartBody(): MultipartForm解析 multipart 表单(含文件)
isMultipartctx.isMultipart(): bool检查是否为 multipart 请求
isJsonctx.isJson(): bool检查是否为 JSON 请求
isFormUrlencodedctx.isFormUrlencoded(): bool检查是否为 URL 编码表单

Context — Cookie 操作

方法签名说明
getCookiesctx.getCookies(): StringTableRef获取所有 Cookie
getCookiectx.getCookie(name): string获取指定 Cookie
getCookieOrDefaultctx.getCookieOrDefault(name, default): string获取 Cookie,带默认值
hasCookiectx.hasCookie(name): bool检查 Cookie 是否存在
setCookiectx.setCookie(key, value, ...)设置响应 Cookie
deleteCookiectx.deleteCookie(key, path, domain)删除 Cookie(设为过期)

Context — 响应设置

方法签名说明
statusctx.status(code: HttpCode)设置状态码
statusctx.status(code: int)设置状态码(int 版本)
setHeaderctx.setHeader(name, value)设置响应头(覆盖)
addHeaderctx.addHeader(name, value)添加响应头(追加)
setContentTypectx.setContentType(contentType)设置 Content-Type
jsonctx.json(data: JsonNode, code = Http200)设置 JSON 响应
jsonctx.json(code: HttpCode, data: JsonNode)设置 JSON 响应(状态码在前)
textctx.text(data: string, code = Http200)设置纯文本响应
htmlctx.html(data: string, code = Http200)设置 HTML 响应
rawctx.raw(body, contentType, code)设置原始响应体
sendFilectx.sendFile(filePath, code, contentType)发送文件(内联显示)
downloadctx.download(filePath, fileName, code)强制下载文件(附件)
downloadBytesctx.downloadBytes(data, fileName, contentType, code)发送字节数据作为下载文件
sendFileChunkedctx.sendFileChunked(filePath, chunkSize, code, contentType)分块传输大文件
serveFileRangectx.serveFileRange(filePath)支持 Range 请求的文件传输
redirectctx.redirect(url, code = Http302)设置重定向
noContentctx.noContent()设置 204 响应
errorResponsectx.errorResponse(code, message)设置 JSON 错误响应

Context — 请求信息

方法签名说明
httpMethodctx.httpMethod(): HttpMethod获取请求方法
pathctx.path(): string获取请求路径
fullUrlctx.fullUrl(): string获取完整 URL
clientIPctx.clientIP(): string获取客户端 IP
isSecurectx.isSecure(): bool检查是否 HTTPS
hostctx.host(): string获取 Host 头
schemectx.scheme(): string获取协议(http/https)
queryStringctx.queryString(): string获取原始查询字符串

中间件与钩子模板

模板说明
app.middleware(body)添加前置中间件,body 中 ctx 可用
app.afterMiddleware(body)添加后置中间件,在路由处理器之后执行
app.exceptionHandler(code, body)注册状态码异常处理器
app.onStartup(body)添加启动钩子
app.onShutdown(body)添加关闭钩子

CORS 配置

方法签名说明
enableCorsapp.enableCors(config: CorsConfig)启用 CORS(完整配置)
enableCorsapp.enableCors(allowOrigins, allowMethods, ...)启用 CORS(简化配置)

CorsConfig 字段:

字段类型默认值说明
allowOriginsseq[string]["*"]允许的源
allowMethodsseq[string]全部方法允许的 HTTP 方法
allowHeadersseq[string]["*"]允许的请求头
allowCredentialsboolfalse是否允许凭证
maxAgeint600预检缓存时间(秒)
exposeHeadersseq[string][]暴露给前端的响应头

静态文件

方法签名说明
mountapp.mount(prefix, directory)挂载静态文件目录

WebSocket API

方法签名说明
sendTextws.sendText(text: string)发送文本消息
sendBinaryws.sendBinary(data: string)发送二进制消息
sendJsonws.sendJson(data: JsonNode)发送 JSON 消息
recvMessagews.recvMessage(): Future[WebSocketMessage]接收消息
pingws.ping(data = "")发送 Ping 帧
pongws.pong(data = "")发送 Pong 帧
closews.close(code = 1000, reason = "")关闭连接
isClosedws.isClosed(): bool检查是否已关闭

MultipartForm API

方法签名说明
getFileform.getFile(name): Option[UploadedFile]获取上传文件(返回 Option,注意与路由 get 冲突时用 form.files[name])
getFieldform.getField(name): string获取表单字段
hasFileform.hasFile(name): bool检查文件是否存在
hasFieldform.hasField(name): bool检查字段是否存在

UploadedFile 字段:filename(文件名)、contentType(MIME 类型)、data(文件内容)。

错误处理

方法签名说明
newHttpErrornewHttpError(code, message = ""): HttpError创建 HTTP 异常对象
raiseHttpErrorraiseHttpError(code, message = "")抛出 HTTP 异常

后台任务

方法签名说明
addBackgroundTaskapp.addBackgroundTask(task: BackgroundTask)添加后台异步任务

API 文档

方法/模板签名说明
enableDocsapp.enableDocs(title, version, description)启用 API 文档
disableDocsapp.disableDocs()禁用 API 文档
docGetapp.docGet(path, desc, tags, body)注册带文档的 GET 路由
docPostapp.docPost(path, desc, tags, body)注册带文档的 POST 路由
docPutapp.docPut(path, desc, tags, body)注册带文档的 PUT 路由
docPatchapp.docPatch(path, desc, tags, body)注册带文档的 PATCH 路由
docDeleteapp.docDelete(path, desc, tags, body)注册带文档的 DELETE 路由
describeLastapp.describeLast(summary, description, tags, deprecated)为最后注册的路由补充文档信息
describeBodyapp.describeBody(bodyKind, fields, description)为最后注册的路由添加请求体文档
describeJsonBodyapp.describeJsonBody(fields, description)快捷方法:添加 JSON 请求体文档
describeFileUploadapp.describeFileUpload(fileField, extraFields, description)快捷方法:添加文件上传文档

HttpCode 常用值

常量状态码含义
Http200200OK
Http201201Created
Http204204No Content
Http301301Moved Permanently
Http302302Found
Http304304Not Modified
Http400400Bad Request
Http401401Unauthorized
Http403403Forbidden
Http404404Not Found
Http405405Method Not Allowed
Http409409Conflict
Http422422Unprocessable Entity
Http500500Internal Server Error
Http503503Service Unavailable

完整示例

以下是一个综合演示 FastNim 所有功能的完整应用,包含 CRUD API、认证中间件、CORS、WebSocket、文件上传、静态文件等:

# complete_app.nim — FastNim 完整示例
import std/[json, tables, times, os, strutils]
import fastnim

# --- 数据模型 ---
type
  User = object
    id: int
    name: string
    email: string

var users = initTable[int, User]()
var nextId = 1
var sessions = initTable[string, int]()  # token -> userId

# --- 创建应用 ---
let app = newApp(port = 8080)

# --- CORS ---
app.enableCors(allowOrigins = @["http://localhost:3000"], allowCredentials = true)

# --- 静态文件 ---
app.mount("/static", "./public")

# --- 生命周期 ---
app.onStartup:
  echo "服务器启动中..."
  app.state["version"] = "1.0.0"

app.onShutdown:
  echo "服务器关闭中..."

# --- 中间件:日志 ---
app.middleware:
  echo "[", $now(), "] ", ctx.httpMethod(), " ", ctx.path()

# --- 中间件:认证(仅 /api 路径) ---
app.middleware:
  if ctx.path().startsWith("/api/") and not ctx.path().startsWith("/api/login"):
    let token = ctx.bearerToken()
    if token.len == 0 or not sessions.hasKey(token):
      ctx.errorResponse(Http401, "未授权访问")
      return
    ctx.state["userId"] = $sessions[token]

# --- 异常处理 ---
app.exceptionHandler(404):
  ctx.json(%*{"error": "资源不存在", "path": ctx.path()}, Http404)

app.exceptionHandler(500):
  ctx.json(%*{"error": "服务器内部错误"}, Http500)

# --- 基础路由 ---
app.get "/":
  ctx.json(%*{
    "name": "FastNim API",
    "version": app.state.getOrDefault("version"),
    "endpoints": ["/api/login", "/api/users", "/ws"]
  })

# --- 登录 ---
app.post "/api/login":
  let body = ctx.jsonBody()
  let email = body["email"].getStr()
  let password = body["password"].getStr()
  if password != "123456":
    ctx.errorResponse(Http401, "密码错误")
    return
  let token = "token-" & $nextId
  sessions[token] = nextId
  ctx.setCookie("session", token, path = "/", httpOnly = true)
  ctx.json(%*{"token": token, "message": "登录成功"})

# --- 用户 CRUD ---
app.get "/api/users":
  var userList: seq[JsonNode] = @[]
  for u in users.values:
    userList.add(%*{"id": u.id, "name": u.name, "email": u.email})
  ctx.json(%*{"users": userList, "count": userList.len})

app.get "/api/users/{id}":
  let id = ctx.pathParamInt("id")
  if not users.hasKey(id):
    raiseHttpError(Http404, "用户不存在")
  let u = users[id]
  ctx.json(%*{"id": u.id, "name": u.name, "email": u.email})

app.post "/api/users":
  let body = ctx.jsonBody()
  let user = User(
    id: nextId,
    name: body["name"].getStr(),
    email: body["email"].getStr()
  )
  users[nextId] = user
  nextId += 1
  ctx.json(%*{"id": user.id, "name": user.name, "email": user.email}, Http201)

app.put "/api/users/{id}":
  let id = ctx.pathParamInt("id")
  if not users.hasKey(id):
    raiseHttpError(Http404, "用户不存在")
  let body = ctx.jsonBody()
  var u = users[id]
  if body.hasKey("name"): u.name = body["name"].getStr()
  if body.hasKey("email"): u.email = body["email"].getStr()
  users[id] = u
  ctx.json(%*{"id": u.id, "name": u.name, "email": u.email})

app.delete "/api/users/{id}":
  let id = ctx.pathParamInt("id")
  if not users.hasKey(id):
    raiseHttpError(Http404, "用户不存在")
  users.del(id)
  ctx.noContent()

# --- 搜索 ---
app.get "/api/users/search":
  let keyword = ctx.queryParam("q")
  var results: seq[JsonNode] = @[]
  for u in users.values:
    if u.name.contains(keyword) or u.email.contains(keyword):
      results.add(%*{"id": u.id, "name": u.name, "email": u.email})
  ctx.json(%*{"results": results, "keyword": keyword})

# --- 文件上传 ---
app.post "/api/upload":
  if not ctx.isMultipart():
    ctx.errorResponse(Http400, "需要 multipart 表单")
    return
  let form = ctx.multipartBody()
  if form.hasFile("file"):
    let file = form.files["file"]
    writeFile("./uploads/" & file.filename, file.data)
    ctx.json(%*{
      "filename": file.filename,
      "size": file.data.len,
      "contentType": file.contentType
    }, Http201)
  else:
    ctx.errorResponse(Http400, "未找到文件")

# --- WebSocket 聊天 ---
app.websocket "/ws":
  await ws.sendText("连接成功!")
  while not ws.isClosed:
    let msg = await ws.recvMessage()
    if msg.kind == wsmText:
      if msg.text == "ping":
        await ws.sendText("pong")
      else:
        await ws.sendText("Echo: " & msg.text)
    elif msg.kind == wsmClose:
      break

# --- 后置中间件:添加通用头 ---
app.afterMiddleware:
  ctx.setHeader("X-Powered-By", "FastNim")

# --- 启动服务器 ---
app.run()
编译运行

将以上代码保存为 complete_app.nim,然后执行:

nim c -r complete_app.nim

服务器将在 http://0.0.0.0:8080 启动。可用 curl 测试:

# 登录获取 token
curl -X POST http://localhost:8080/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"a@b.com","password":"123456"}'

# 创建用户(使用返回的 token)
curl -X POST http://localhost:8080/api/users \
  -H "Authorization: Bearer token-1" \
  -H "Content-Type: application/json" \
  -d '{"name":"张三","email":"zhang@example.com"}'

# 获取用户列表
curl http://localhost:8080/api/users \
  -H "Authorization: Bearer token-1"
项目结构建议
myapp/
├── complete_app.nim      # 主程序
├── public/               # 静态文件目录
│   └── index.html
├── uploads/              # 上传文件目录
└── nim.cfg               # 编译配置