原作者:Andy Jiang;原文发表于 2023 年 1 月 11 日,来源:Deno Blog — How to Build a GraphQL Server with Deno。本文按授权翻译整理,原示例与编者修订分别标明。
GraphQL 是构建 API 的常见方式。客户端可以只请求需要的字段,减少多余数据传输,并在适合的数据模型中减少连续等待的请求,从而改善客户端性能。本文将用 Deno 构建一个 GraphQL API 服务器。完整示例在 denoland/deno-graphql。原文也提到,可以通过 npm specifier 使用 Apollo;那是另一种实现选择,本教程使用下面列出的 URL 导入依赖。

一、先让服务器回答 Hello World
新建 hello_world_server.ts,导入四个依赖:Server 创建 HTTP 服务器;GraphQLHTTP 根据模式创建 HTTP 中间件;makeExecutableSchema 合并类型定义和解析器;gql 用于书写类型定义。
import { Server } from "https://deno.land/std@0.166.0/http/server.ts";
import { GraphQLHTTP } from "https://deno.land/x/gql@1.1.2/mod.ts";
import { makeExecutableSchema } from "https://deno.land/x/graphql_tools@0.0.2/mod.ts";
import { gql } from "https://deno.land/x/graphql_tag@0.0.1/mod.ts";
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => `Hello, World!`,
},
};
const schema = makeExecutableSchema({ resolvers, typeDefs });
const server = new Server({
handler: async (req) => {
const { pathname } = new URL(req.url);
return pathname === "/graphql"
? await GraphQLHTTP<Request>({ schema, graphiql: true })(req)
: new Response("Not Found", { status: 404 });
},
port: 3000,
});
server.listenAndServe();
Query 的 hello 字段返回字符串;对应解析器决定这个字段实际得到什么值。查询和变更的行为都由解析器实现。makeExecutableSchema 把类型与实现合成可执行模式,再交给 GraphQLHTTP。
HTTP 处理函数从请求 URL 取出路径,仅在路径严格等于 /graphql 时调用 GraphQL 中间件;其他路径返回 404。graphiql: true 打开 GraphiQL 交互界面。也可以关闭该界面,改用 Postman、Insomnia 或 curl 发送请求。最后一行让服务监听 3000 端口。
deno run --allow-net hello_world_server.ts
--allow-net 授予网络访问权限。按原文运行后,在浏览器访问 http://localhost:3000/graphql,可以在 GraphiQL 中输入:
query {
hello
}
按上述解析器,预期响应如下。原文响应片段写成了 Hello World!,少了逗号;这里按代码修正了这处展示不一致,并非实测输出。
{
"data": {
"hello": "Hello, World!"
}
}
二、把查询连接到真实数据
GraphQL 的价值不止在返回固定字符串。接下来扩展示例,让客户端可以查询数据、挑选返回字段,并往数据库中插入记录。原文选择 PostgreSQL,原文推荐的数据库搭建资料是其 Postgres 连接教程(该旧链接在本次核对时返回 404);示例使用仓库中的 dinosaurs.json 恐龙数据。表需要能够存储 name 和 description。原文没有在本页给出建表与导入命令,因此这里也不把未核验的建库脚本当作原文步骤。
除了新增用于写入的 mutation,整体结构与 Hello World 相同。为了让文件更清晰,先把类型定义与 gql 导入移到 typedefs.ts:
import { gql } from "https://deno.land/x/graphql_tag@0.0.1/mod.ts";
export const typeDefs = gql`
type Query {
allDinosaurs: [Dinosaur]
oneDinosaur(name: String): Dinosaur
}
type Dinosaur {
name: String
description: String
}
type Mutation {
addDinosaur(name: String, description: String): Dinosaur
}
`;
allDinosaurs 返回恐龙列表,方括号表示列表类型;oneDinosaur 按名称返回一只恐龙。Dinosaur 包含名称和描述两个字符串字段。addDinosaur 是添加记录的变更。GraphQL 名称区分大小写,原文解释段落里的 OneDinosaur 应以代码中的 oneDinosaur 为准。没有感叹号的字段与参数可以为空,这是此示例的模式设计,不等于所有业务都应该接受空值。
三、拆出解析器与数据库操作
新建 resolvers.ts,把查询、变更和 PostgreSQL 连接放在这里。下面保留原文的完整逻辑,便于对照其思路;紧接着说明静态审核发现的问题。
import * as postgres from "https://deno.land/x/postgres@v0.14.2/mod.ts";
const connect = async () => {
const databaseUrl = Deno.env.get("DATABASE_URL")!;
const pool = new postgres.Pool(databaseUrl, 3, true);
const connection = await pool.connect();
return connection;
};
const allDinosaurs = async () => {
const connection = await connect();
const result = await connection.queryObject`
SELECT name, description FROM dinosaurs
`;
return result.rows;
};
const oneDinosaur = async (args: any) => {
const connection = await connect();
const result = await connection.queryObject`
SELECT name, description FROM dinosaurs WHERE name = ${args.name}
`;
return result.rows;
};
const addDinosaur = async (args: any) => {
const connection = await connect();
const result = await connection.queryObject`
INSERT INTO dinosaurs(name, description)
VALUES(${args.name}, ${args.description})
RETURNING name, description
`;
return result.rows[0];
};
export const resolvers = {
Query: {
allDinosaurs: () => allDinosaurs(),
oneDinosaur: (_: any, args: any) => oneDinosaur(args),
},
Mutation: {
addDinosaur: (_: any, args: any) => addDinosaur(args),
},
};
Deno.env.get("DATABASE_URL") 从环境变量读取 PostgreSQL 连接串。原例用 new postgres.Pool(databaseUrl, 3, true) 建立最多三个连接的惰性连接池,再取得一个连接。访问环境变量时需要另授予 --allow-env。
allDinosaurs 查询表中的名称和描述;oneDinosaur 接收名称参数并加入 WHERE 条件;addDinosaur 插入名称和描述,通过 RETURNING 返回刚插入的记录。解析器再把 GraphQL 参数转交给这些函数。这里的 SQL 是驱动提供的带标签模板,插值作为查询参数处理,不是手工拼接 SQL 文本;不要为了“简化”而改成字符串拼接。
以下是与原例明确区分的修订骨架:在模块级共享连接池,启动时检查配置,并在 finally 中归还连接。单条查询改为返回第一条记录或 null。其他两个操作同样需要在 finally 中调用 release()。这段只经过静态检查,未验证旧依赖与当前运行时的组合。
const databaseUrl = Deno.env.get("DATABASE_URL");
if (!databaseUrl) throw new Error("DATABASE_URL is required");
const pool = new postgres.Pool(databaseUrl, 3, true);
type Dinosaur = { name: string; description: string };
const oneDinosaur = async (args: { name?: string }) => {
if (typeof args.name !== "string" || args.name.length === 0) {
throw new Error("A non-empty name is required");
}
const connection = await pool.connect();
try {
const result = await connection.queryObject<Dinosaur>`
SELECT name, description FROM dinosaurs WHERE name = ${args.name}
`;
return result.rows[0] ?? null;
} finally {
connection.release();
}
};
如果名称在业务上应当唯一,需要在数据库层建立相应约束;否则“取第一条”不能定义稳定的业务含义。添加记录也应校验字段长度和必填条件。服务退出时还需要处理连接池关闭,这超出了原文的演示范围。
四、组装服务器并发送查询与变更
将主文件改名为 server.ts。类型和解析器移出去之后,主文件只需改变导入,其他结构仍然一样:
import { Server } from "https://deno.land/std@0.166.0/http/server.ts";
import { GraphQLHTTP } from "https://deno.land/x/gql@1.1.2/mod.ts";
import { makeExecutableSchema } from "https://deno.land/x/graphql_tools@0.0.2/mod.ts";
import { resolvers } from "./resolvers.ts";
import { typeDefs } from "./typedefs.ts";
const schema = makeExecutableSchema({ resolvers, typeDefs });
const server = new Server({
handler: async (req) => {
const { pathname } = new URL(req.url);
return pathname === "/graphql"
? await GraphQLHTTP<Request>({ schema, graphiql: true })(req)
: new Response("Not Found", { status: 404 });
},
port: 3000,
});
server.listenAndServe();
deno run --allow-net --allow-env server.ts
重新访问 http://localhost:3000/graphql,按原文过程,GraphiQL 的自动补全会出现新增查询与变更。以下查询请求所有恐龙的名称和描述:
query {
allDinosaurs {
name
description
}
}
GraphQL 的客户端可以选择需要的字段。只需名称时,删掉 description:
query {
allDinosaurs {
name
}
}
要查询 Aardonyx,则把名称传给 oneDinosaur。原例的单条返回值问题需要先按上文修正:
query {
oneDinosaur(name: "Aardonyx") {
name
description
}
}
添加一只名为 Deno 的恐龙,可以执行以下变更,同时要求 API 返回插入后的字段:
mutation {
addDinosaur(name: "Deno", description: "the fastest Deno in the world") {
name
description
}
}
原文接着再次调用 oneDinosaur(name: "Deno") 查看记录,作为交互演示中的核对步骤。本文没有实际执行该写入,也不把原文截图或以上预期行为视为本次测试结果。
五、继续扩展之前要明确的边界
这个服务器展示了 Deno 中 GraphQL 的基本实现,还可以增加更多类型、查询与数据关系。例如原来的每只恐龙互相独立,加入 Clade(演化支)类型后,就能表达 Aardonyx、Seitaad 和 Sefapanosaurus 的关系。也可以为这些数据建立前端。原文推荐其 Shopify GraphQL 商店案例 How to Build an E-commerce Site with a Perfect Lighthouse Score(原链接当前显示 404),并邀请读者到 Deno 社区交流。
在对外提供服务前,需要为查询与变更增加身份认证、授权、限流和查询复杂度限制。原示例的 mutation 可以写数据库,不能未经保护地暴露公网;GraphiQL 也是调试入口。原命令的网络和环境权限范围较大,应按实际监听地址、数据库端点与变量名缩小权限。连接串应由部署环境注入,不应硬编码进源码或提交仓库。TLS、数据库账户的最小权限、分页和错误信息处理也需要另外设计。
原文网页版权归 Deno Land Inc.;代码的具体授权以关联仓库的许可文件为准,网页版权声明不自动等同于代码许可证。原创示意图与审核注释由未完纪编辑整理。












暂无评论内容