Wasp 自定义 HTTP API 端点(api 声明)完整实战指南:路由、认证、中间件与实体注入
【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp
本篇指南围绕 Wasp(当前仓库为
GitHub_Trending/wa/wasp)中通过api声明创建自定义 HTTP API 端点的完整流程展开。你将掌握在.wasp文件中声明 API、用 Express 风格的 NodeJS 函数实现它、从客户端或外部调用它、通过apiNamespace与middlewareConfigFn精确控制 CORS 与中间件、在context中注入实体与用户会话信息的全部细节。读完即可在 Wasp 项目中写出可复用的自定义 REST 端点(包括流式响应场景)。
Wasp 默认的客户端—服务端交互机制是 Operations(query/action,详见 Operations 概览)。但当你需要特定的 URL 方法/路径组合、特定的响应格式,或需要完全掌控一个端点的行为时,Operations 就不再合适。此时你应当使用api声明——它把一段 JS/TS 函数绑定到形如POST /something/special的 HTTP 端点上。与 Operations 不同,api没有任何客户端辅助函数(如useQuery),但它仍然可以像普通 Express 路由一样被浏览器、curl、Postman 或任意 Web 服务直接调用,也能通过 Wasp 提供的 HTTP 客户端从你自己的前端调用。
如何创建一个 API
创建一个 Wasp API 只需要两步:
- 在 Wasp 文件中用
api声明描述这个端点; - 编写它的 NodeJS 实现函数。
完成这两步后,你就可以从客户端代码(通过 Wasp 的 HTTP 客户端包装器)或从外部世界调用这个 API 了。
在 Wasp 文件中声明 API
在main.wasp中使用api声明即可定义端点。API 声明与它的实现不需要同名(当然同名也可以),下面是一个最简单的示例:
// ... api fooBar { // API 与其实现不必(但可以)同名。 fn: import { fooBar } from "@server/apis.js", httpRoute: (GET, "/foo/bar") }fn指向实现函数的 import 语句;httpRoute是一个(HttpMethod, string)元组,string是 Express 风格的路由路径。
关于各字段的完整说明见后文 API Reference。
定义 API 的 NodeJS 实现
:::note 对 TypeScript 用户:为了确保 Wasp 编译器为 API 生成可供实现使用的类型,请先把api声明写进.wasp文件,并保持wasp start运行。Wasp 会根据声明自动生成@wasp/apis/types中的类型(在 0.11.8 版本中,实现文件中通过import { FooBar } from "@wasp/apis/types"引入)。 :::
实现函数接收三个参数:
req:Express Request 对象;res:Express Response 对象;context:由 Wasp 注入的附加上下文对象,包含用户会话信息以及实体信息。为简洁起见,下面例子暂不使用context,其详细用法见 在 API 中使用实体。
import { FooBar } from "@wasp/apis/types"; // 该类型由 Wasp 基于上面的 `api` 声明自动生成。 export const fooBar: FooBar = (req, res, context) => { res.set("Access-Control-Allow-Origin", "*"); // 示例:修改响应头以覆盖 Wasp 默认 CORS 中间件。 res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` }); };JavaScript 版本同样简单(无需类型导入):
export const fooBar = (req, res, context) => { res.set("Access-Control-Allow-Origin", "*"); res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` }); };这个实现就是一个标准 Express 请求处理器:你可以像在任意 Express 应用中一样读取req、设置响应头、返回 JSON。
为 API 提供额外类型信息(TypeScript)
假设你想创建一个GET路由,它从 URL 参数中接收一个 email 地址,并返回"生命、宇宙以及一切"的答案——在 TypeScript 中长这样:
先在 Wasp 中声明 API:
api fooBar { fn: import { fooBar } from "@server/apis.js", entities: [Task], httpRoute: (GET, "/foo/bar/:email") }然后在实现中使用FooBar泛型,传入params与response两个类型参数,即可获得完整的类型安全:
import { FooBar } from "@wasp/apis/types"; export const fooBar: FooBar< { email: string }, // params { answer: number } // response > = (req, res, _context) => { console.log(req.params.email); res.json({ answer: 42 }); };此时req.params.email的类型会被推导为string,而res.json(...)的入参类型也会被约束为{ answer: number }。这一机制源于 Wasp 生成的 SDK 类型:在仓库中查看 SDK 的 API 类型模板,可以看到 Wasp 会为每个api声明生成一个带P extends ExpressParams = ExpressParams、ResBody = any、ReqBody = any等泛型参数的别名类型(0.11.8 模板位于 Apis 类型生成模板),这正是泛型FooBar<Params, ResBody>的底层来源。
使用 API
从外部使用 API
从外部调用非常简单:直接使用你声明的 HTTP 方法与路径发起请求即可。例如你的应用运行在https://example.com,那么上面的声明对应GET https://example.com/foo/bar(文档原文示例为/foo/callback,请以你声明的路径为准),可以在浏览器、Postman、curl或任意 Web 服务中调用。
从客户端使用 API
从客户端调用自定义 API(包括携带认证信息)时,可以导入@wasp/api提供的 Axios 包装器:
import React, { useEffect } from "react"; import api from "@wasp/api"; async function fetchCustomRoute() { const res = await api.get("/foo/bar"); console.log(res.data); } export const Foo = () => { useEffect(() => { fetchCustomRoute(); }, []); return <>// ...</>; };TypeScript 版本完全一致:
import React, { useEffect } from "react"; import api from "@wasp/api"; async function fetchCustomRoute() { const res = await api.get("/foo/bar"); console.log(res.data); } export const Foo = () => { useEffect(() => { fetchCustomRoute(); }, []); return <>// ...</>; };仓库中的 kitchen-sink 示例提供了一个真实的落地样例:ApisPage.tsx 通过api.get(endpoint).json()分别请求需要认证的/foo/bar与无需认证的/bar/baz,并用useQuery包装以展示 loading / error / data 三种状态。配套的 e2e 测试 验证了"未登录时认证 API 返回错误、/bar/baz正常返回Hello, stranger!;登录后认证 API 返回Hello, <email>!"的完整行为,可以直接作为你端到端验证自定义 API 的参考。
确保 CORS 正常工作
API 被设计为尽可能灵活,因此它们不像 Operations 那样默认挂载中间件。要在客户端正常使用这些 API,你必须确保 CORS(跨域资源共享)被启用。
做法是在 Wasp 文件中为 API 定义自定义中间件。例如apiNamespace就是一种简单声明,用来把某个middlewareConfigFn应用到某个路径下的所有 API:
apiNamespace fooBar { middlewareConfigFn: import { fooBarNamespaceMiddlewareFn } from "@server/apis.js", path: "/foo" }然后在实现文件中返回默认配置(TS 版本引入MiddlewareConfigFn类型):
import { MiddlewareConfigFn } from "@wasp/middleware"; export const apiMiddleware: MiddlewareConfigFn = (config) => { return config; };返回默认中间件配置,即表示/foo路径下的所有 API 都启用 CORS。更完整的中间件定制说明见 中间件配置。
从源码层面看,apiNamespace在 ApiNamespace.hs 中被定义为仅含middlewareConfigFn :: ExtImport与path :: String两个字段的数据结构。生成阶段会把它编译为router.use('<path>', globalMiddlewareConfigForExpress(...))(见 生成模板),即挂在路由层级的路径级中间件。
在 API 中使用实体
多数情况下,API 中要操作的资源都是 实体(Entity)。要把实体注入 API,只需在api声明的entities字段中列出它们:
api fooBar { fn: import { fooBar } from "@server/apis.js", entities: [Task], httpRoute: (GET, "/foo/bar") }Wasp 会把列出的实体注入 API 的context参数,从而让你直接访问该实体的 Prisma API:
import { FooBar } from "@wasp/apis/types"; export const fooBar: FooBar = (req, res, context) => { res.json({ count: await context.entities.Task.count() }); };context.entities.Task暴露的就是 Prisma CRUD API 中的prisma.task。从生成代码看,这一注入由 ApiRoutesG.hs 中的getApiEntitiesObject完成,最终在 生成模板 中表现为构造context.entities = { Task: prisma.task, ... }传给实现函数。kitchen-sink 示例中,apis.wasp.ts 的/foo/bar与/bar/baz两个 API 都声明了entities: ["Task"]。
API 中auth字段与context.user
api声明中的auth: bool字段控制该端点是否解析 JWT:
- 当项目启用了认证时,
auth默认为true,实现函数的context中会提供context.user对象; - 如果你不希望该端点尝试解析 Authorization Header 中的 JWT(例如公开的 webhook 回调),请显式设置为
false。
从实现看,ApiRoutesG.hs 中的isAuthEnabledForApi spec api = fromMaybe (isAuthEnabled spec) (Api.auth api)表明:API 的auth取值优先于全局认证开关——未显式声明时回退到项目全局是否启用认证。生成模板中,启用认证的路由会被编译为router.<method>(path, [auth, ...middleware], defineHandler(...)),并把makeAuthUserIfPossible(req.user)的结果放进context.user(见 生成模板)。
API Reference
api声明的完整字段如下(完整示例见 apis.wasp.ts):
api fooBar { fn: import { fooBar } from "@server/apis.js", httpRoute: (GET, "/foo/bar"), entities: [Task], auth: true, middlewareConfigFn: import { apiMiddleware } from "@server/apis.js" }fn: ServerImport(必填)该 API NodeJS 实现的 import 语句。
httpRoute: (HttpMethod, string)(必填)HTTP 方法与路径的二元组。方法可以是
ALL、GET、POST、PUT、DELETE;路径是 Express 路径字符串(支持:param、通配符等 Express 语法)。在 Api.hs 中,该字段被定义为(HttpMethod, String),HttpMethod数据构造器恰好为ALL | GET | POST | PUT | DELETE,且编译器会在 Valid.hs 的validateApiRoutesAreUnique中校验所有 API 的(方法、路径)组合唯一——同一路径上声明相同方法或声明ALL会与其他方法构成冲突并报错"apiroutes must be unique"。entities: [Entity]希望在 API 内部使用的实体列表,会注入
context.entities,详见 在 API 中使用实体。auth: bool启用认证时默认
true,并提供context.user对象。如果不想解析 Authorization Header 中的 JWT,设置为false。middlewareConfigFn: ServerImport该 API 的 Express 中间件配置函数 import 语句。未指定时使用默认中间件(在生成模板中以
idFn兜底,见 生成模板);指定后可以middlewareConfig.set/delete增删中间件。更多说明见 中间件配置。
进阶:用中间件定制一个"非默认"的 API
由于api不使用 Operations 的默认中间件链,你可以针对单个 API 完全替换其中的中间件,这在处理 webhook 等场景时尤其有用。例如下面这个 webhook 回调,将express.json替换为接收任意原始内容的express.raw:
api webhookCallback { fn: import { webhookCallback } from "@server/apis.js", middlewareConfigFn: import { webhookCallbackMiddlewareFn } from "@server/apis.js", httpRoute: (POST, "/webhook/callback"), auth: false }import express from 'express' import { WebhookCallback } from '@wasp/apis/types' import type { MiddlewareConfigFn } from '@wasp/middleware' export const webhookCallback: WebhookCallback = (req, res, _context) => { res.json({ msg: req.body.length }) } export const webhookCallbackMiddlewareFn: MiddlewareConfigFn = (middlewareConfig) => { middlewareConfig.delete('express.json') middlewareConfig.set('express.raw', express.raw({ type: '*/*' })) return middlewareConfig }kitchen-sink 示例的 apis.ts 完整复现了这个模式(fooBarMiddlewareFn用set("custom.route", ...)追加自定义中间件、webhookCallbackMiddlewareFn用delete/set替换express.json),并且该 API 挂载在单条路由上;而barNamespaceMiddlewareFn则展示了如何通过apiNamespace为/bar下所有 API 统一注入中间件。其默认中间件集合(helmet、cors、morgan、express.json、express.urlencoded、cookieParser)及各层级的定制方式详见 中间件配置。
流式响应(Streaming)场景
自定义 API 的另一个典型用途是流式响应:利用 Express 的res.write()/res.end()把数据分块推送给客户端。在生成模板中,实现函数被defineHandler包裹后直接作为路由处理器挂载(见 生成模板),因此原生 Express 的流式写法天然可用。kitchen-sink 示例提供了完整可运行样例:
export const streamingText: StreamingText = async (_req, res, _context) => { res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Transfer-Encoding", "chunked"); res.setHeader("Cache-Control", "no-transform"); // 防止代理(如边缘 CDN)压缩缓冲流 res.write("Hm, let me see...\n"); // ...循环 res.write() 分块发送 res.end(); };对应地在 Wasp 文件中声明,并确保为该路径启用 CORS 中间件:
api("GET", "/api/streaming-test", streamingText), apiNamespace("/api/streaming-test", { middlewareConfigFn: defaultMiddlewareForStreamingText, }),客户端通过fetch的response.bodyReadableStream 逐块读取内容(见 StreamingTestPage.tsx),即可实现"边生成边展示"的效果——这正是 AI 场景下流式输出 LLM 回复的典型实现路径。
小结
Wasp 的api声明在保留 Operations 便捷性的同时,把"端点的完全控制权"交还给了开发者:
- 两步即可上线一个自定义 REST 端点:
.wasp中声明 + NodeJS 实现; - 通过
context统一获得用户会话(context.user)与实体 Prisma API(context.entities); - 用
middlewareConfigFn/apiNamespace精确控制 CORS 与中间件,应对 webhook、原始 body、流式响应等特殊需求; - 编译器自动校验路由唯一性,并生成类型安全的 SDK 类型,全程享受 TS 类型保障。
相关参考:Operations 概览、实体、中间件配置、示例实现 apis.wasp.ts 与 apis.ts。
【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考