Next.js 集成 Neo4j 实战:用 API Routes 构建电影图谱应用
本文以 examples/with-neo4j 官方示例为主线,系统讲解如何在 Next.js 应用中通过 API Routes 接入 Neo4j 图数据库:从创建数据库、灌入电影示例数据、配置环境变量,到 Cypher 查询在服务端 API 中的落地方式,以及前端基于 SWR 的渲染链路。读完你可以完整复现一个"人物—电影—关系"的图谱 Web 应用,并理解 neo4j-javascript-driver 在服务端会话与事务的正确用法。
with-neo4j 是 Next.js 官方仓库中演示"Next.js + 图数据库"的经典示例:它使用 Neo4j 官方的 Movies 示例数据集(Movie 节点、Person 节点,以及 ACTED_IN、DIRECTED、PRODUCED、REVIEWED 等关系类型),通过 3 个 API 路由暴露查询能力,前端页面则负责把关系型数据渲染成可供点击跳转的影片列表、演员详情页与影片详情页。
示例整体结构
先看这个示例在仓库中的文件布局(目录:examples/with-neo4j):
examples/with-neo4j/
├── components/
│ ├── footer.js # 页脚
│ └── header.js # 页头,支持传入标题
├── lib/
│ └── fetcher.js # SWR 的全局 fetch 封装,返回 res.json()
├── pages/
│ ├── _app.js # 注入全局样式
│ ├── index.js # 首页:影片列表
│ ├── actor/[name].js # 演员详情页(动态路由)
│ ├── movie/[title].js # 影片详情页(动态路由)
│ └── api/
│ ├── movies/index.js # GET /api/movies → 全部影片
│ ├── movies/[title].js # GET /api/movies/:title → 单部影片
│ └── actors/[name].js # GET /api/actors/:name → 单个演员
├── util/
│ └── neo4j.js # 创建并缓存 Neo4j 驱动(单例)
├── styles/
│ └── global.js
├── .env.local.example # 环境变量模板
├── movie-sample.md # 可选的 Cypher 电影样本数据
└── package.json
从结构上可以清晰看出本示例的核心架构模式:
- 服务端查询集中在
pages/api/*(API Routes)。Node.js 运行时天然适合承载 Neo4j 官方驱动,所有 Cypher 查询都运行在服务端,避免把数据库凭据暴露到浏览器。 - 浏览器渲染使用 SWR(
swr@^2.0.0)请求这些 API,并配合动态路由展示影片/演员的详情与互链关系。 - 依赖清单(见 package.json)非常精简:
next、react、react-dom、neo4j-driver@^4.3.1、swr@^2.0.0,脚本为标准的dev/build/start。
快速启动示例
推荐用 create-next-app 直接以该示例为模板初始化项目(示例 CLI 位于 packages/create-next-app),三种包管理器任选其一:
npx create-next-app --example with-neo4j with-neo4j-app
yarn create next-app --example with-neo4j with-neo4j-app
pnpm create next-app --example with-neo4j with-neo4j-app
命令执行后会在当前目录生成 with-neo4j-app 子项目,随后进入目录安装依赖并启动开发服务器:
cd with-neo4j-app
npm install # 或 yarn / pnpm install
npm run dev # 或 yarn dev / pnpm dev
启动后访问 http://localhost:3000 即可看到影片列表首页。注意:在配置好数据库连接之前,页面与 API 会因无法连接 Neo4j 而返回加载失败或空数据,所以接下来需要完成数据库准备。
前提条件:创建 Neo4j 数据库并写入 Movies 数据
Step 1:创建 Neo4j 数据库
本示例需要一台可达的 Neo4j 实例。官方示例中推荐了免费方案:Neo4j Desktop(本地桌面版,自带空数据库实例)以及 Neo4j Sandbox / AuraDB(在线托管)。无论是本地实例还是云端实例,都需要记下三个连接要素:
- 连接 URI(例如
bolt://localhost:7687,或云端实例提供的neo4j+s://xxxx.databases.neo4j.io) - 用户名(默认通常是
neo4j) - 密码
Step 2:灌入电影图谱数据
示例约定数据库中存在 Movies 图模型。Neo4j 官方在这些工具中内置了一个标准 Movies 数据集,可以在 Neo4j Browser 中通过内置教程引导加载:
:play movie-graph
如果你想用一段独立的 Cypher 脚本一次性建好全量数据,示例还附带了一份电影样本查询 movie-sample.md。该文件定义了两类节点与五类关系:
- 节点:
Movie(属性含title、released、tagline)、Person(属性含name、born) - 关系:
ACTED_IN(角色/演员参演)、DIRECTED(导演)、PRODUCED(制片)、WROTE(编剧)、FOLLOWS(关注)、REVIEWED(评论,含summary与rating)
其 Cypher 写法以《黑客帝国》三部曲为开篇,例如:
CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})
CREATE (Keanu:Person {name:'Keanu Reeves', born:1964})
CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967})
CREATE
(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix),
(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix),
(LillyW)-[:DIRECTED]->(TheMatrix),
(JoelS)-[:PRODUCED]->(TheMatrix)
Step 3:配置环境变量
复制示例目录下的环境变量模板为 .env.local(该文件会被 Git 忽略,不会进入版本库):
cp .env.local.example .env.local
.env.local.example(查看原文件)中一共声明了三个变量:
# Environment variables required to connect the app with your Neo4j database
NEO4J_URI=
NEO4J_USER=
NEO4J_PASSWORD=
将它们分别填上你数据库的实际 URI、用户名与密码即可。这三个变量正是 util/neo4j.js 中读取的连接参数来源:
const defaultOptions = {
uri: process.env.NEO4J_URI,
username: process.env.NEO4J_USER,
password: process.env.NEO4J_PASSWORD,
};
因此在本地,三个变量的示例取值形如:
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=your_password
服务端连接管理:驱动单例与整数处理
示例把连接逻辑收敛在 util/neo4j.js 这一个文件中,核心是惰性单例 + 会话事务:
import neo4j from "neo4j-driver";
let driver;
const defaultOptions = {
uri: process.env.NEO4J_URI,
username: process.env.NEO4J_USER,
password: process.env.NEO4J_PASSWORD,
};
export default function getDriver() {
const { uri, username, password } = defaultOptions;
if (!driver) {
driver = neo4j.driver(uri, neo4j.auth.basic(username, password), {
disableLosslessIntegers: true,
});
}
return driver;
}
几个值得展开的实现细节:
let driver模块级缓存:由于各 API 路由(pages/api/movies/index.js、pages/api/movies/[title].js、pages/api/actors/[name].js)都从同一模块导入getDriver,模块作用域中的driver变量在整个服务进程内共享,因此驱动只被创建一次,重复请求复用同一个连接池。可以推断:这正是示例有意避免为每个请求重复创建 driver 的设计——driver 在官方驱动中本应作为长生命周期对象被复用。disableLosslessIntegers: true:Neo4j 服务端返回的整数在驱动中默认包装为 Neo4j 的 Long(无损整数)类型,前端要拿到普通 JS number 需要逐个调用.toNumber()。示例源码注释明确说明启用该选项是为了省去对每个整数值调用.toNumber()的繁琐操作(released、born、rating等属性即可直接作为普通数字使用),代价是放弃超过 JS 安全整数范围的超大整数支持。这是采用图查询返回值直接序列化为 JSON 传给前端时非常实用的一个配置。neo4j.auth.basic(username, password):使用用户名密码的基础认证方式连接数据库。
在此基础上,每个 API 路由文件在模块顶层就建立了一次会话(session):
const driver = getDriver();
const session = driver.session();
并在每个请求处理函数内通过 session.readTransaction(...) 开启只读事务,驱动会在事务提交/回滚后管理好会话的生命周期。
三个 API 路由与 Cypher 查询实现
示例的全部查询能力由三个 API Routes 暴露,它们只响应 GET 方法,其他方法一律返回 400 { success: false }。
1. GET /api/movies —— 影片列表
实现位于 pages/api/movies/index.js。它通过模式理解(pattern comprehension)在一次 Cypher 中同时取出影片自身属性、参演演员名单与导演名单,并按片名升序排列:
MATCH (movie:Movie)
RETURN movie {.*,
actors: [ (movie)<-[:ACTED_IN]-(actor) | actor.name ],
directed: [ (movie)<-[:DIRECTED]-(director) | director.name ]
} as movie
ORDER BY movie.title ASC
其中 movie {.*, ...} 是 Cypher map projection:.* 表示展开节点的全部属性(title、released、tagline),actors 与 directed 两个键则通过 [(movie)<-[:REL]-(x) | x.name] 这种列表模式理解把"反向关联到该影片的演员/导演姓名"收集成字符串数组。这样返回的每条 movie 已经是一份可直接序列化的 JSON 对象。
处理逻辑为:开启只读事务执行 Cypher,将每条记录的 movie 取出并放入数组,最后响应 200 { success: true, movies };任何查询异常则落入 catch 返回 400 { success: false }。
2. GET /api/movies/:title —— 单部影片详情
实现位于 pages/api/movies/[title].js。动态段 title 从路由 query 中读取,并先经过 decodeURIComponent(title) 还原——因为影片标题会作为 URL 路径参数传递(例如 The Devil's Advocate 这类含空格与特殊字符的标题)。
查询使用参数化 Cypher,避免字符串拼接注入:
MATCH (movie:Movie {title: $movieTitle})
RETURN movie {.*,
actors: [ (actor)-[:ACTED_IN]->(movie) | actor.name ],
directed: [ (director)-[:DIRECTED]->(movie) | director.name ]
} as movie
注意这里的关系方向与列表接口相反(从关联节点指向影片),取 records 的第一条作为 movie 返回。
3. GET /api/actors/:name —— 演员详情
实现位于 pages/api/actors/[name].js,逻辑与影片详情对称:先 decodeURIComponent(name),再按 name 精确匹配 Person 节点,并通过 ACTED_IN 关系反查其参演过的全部影片标题:
MATCH (actor:Person {name: $actorName})
RETURN actor {.*,
movies: [ (actor)-[:ACTED_IN]->(m) | m.title ]
} as actor
结果中的 actor 对象包含 name、born 以及 movies 数组,供详情页渲染演员基本信息与作品列表。
前端渲染链路:SWR 拉取 + 动态路由跳转
浏览器侧由 SWR 发起数据请求,统一使用 lib/fetcher.js 这个极简封装作为数据源:
export default async function fetcher(...args) {
const res = await fetch(...args);
return res.json();
}
首页:影片总览
pages/index.js 挂载即请求 /api/movies:
const { data, error, isLoading } = useSWR("/api/movies", fetcher);
if (error) return <div>failed to load</div>;
if (isLoading) return <div>loading...</div>;
if (!data) return null;
拿到 data.movies 后渲染表格,每行包含片名、上映年份、标语、导演列表与演员列表。影片标题与演员姓名都用 <Link> 包成可点击链接,跳转时使用 encodeURIComponent 处理 URL:
<Link
href={`/movie/${encodeURIComponent(movie.title)}`}
className="link"
>
{movie.title}
</Link>
<Link
href={`/actor/${encodeURIComponent(actor)}`}
className="link"
>
{actor}
</Link>
影片详情页
pages/movie/[title].js 从 useRouter() 读取 router.query.title,再请求 /api/movies/${title}(SWR 的 key 是模板字符串,title 变化会自动重新请求),展示 tagline、上映年份、演员(再次链接到演员详情)与导演名单。
演员详情页
pages/actor/[name].js 请求 /api/actors/${name},展示出生年份与其参演影片列表,影片名称可继续跳回对应影片详情页。示例中还演示了一种把"动态路由 + 动态路径"合并使用的 Link 写法:
<Link
href="/movie/[title]"
as={{
pathname: `/movie/${encodeURIComponent(movie)}`,
}}
>
{movie}
</Link>
页面统一通过 pages/_app.js 引入 styles/global.js 中的全局样式,并用 components/header.js 的 title prop 区分"欢迎页"与"详情页"两种标题形态。
至此,完整的数据流为:浏览器 SWR → Next.js API Routes(服务端 Neo4j 驱动 + Cypher)→ Neo4j 图数据库 → 结构化 JSON 返回 → React 渲染 + 动态路由互跳。图数据库中"关系"的表达能力(一部影片的演员与导演、一名演员的所有作品)被天然映射为页面的导航网络,这正是图模型在 Web 应用中典型的落地形态。
部署到云端
部署本地项目
将项目推送到 Git 远程仓库后,在 Vercel(或任意支持 Node.js 的服务平台)中导入该仓库即可完成部署。由于 .env.local 不会进入版本库,在云平台必须手动补齐环境变量:在项目的 Environment Variables 配置中逐项添加 NEO4J_URI、NEO4J_USER、NEO4J_PASSWORD,并确保值与本地 .env.local 一致,同时数据库地址需要是该平台能够访问到的实例(例如 Neo4j Aura 托管实例)。若使用 Next.js 官方模板方式部署,平台同样会在创建项目时要求预先填写这三项环境变量,其含义与 .env.local.example 完全对应。
一个部署注意事项
本示例的 API 路由在模块顶层同步调用 getDriver() 并创建 session。因此部署到 Serverless 或无服务器函数环境时,需要留意:模块初始化发生在函数冷启动阶段,环境变量必须在该阶段就已注入;而请求级事务通过 session.readTransaction() 执行。对于生产环境的高并发场景,通常需要结合平台特性管理连接池与函数的生命周期,但本示例作为入门模板,默认以长驻 Node.js 进程(本地 next start 或传统 Node 部署)为前提即可顺畅运行。
小结
with-neo4j 示例用最少量的代码完整示范了三条可迁移到真实项目中的经验:
- 把图查询收敛在 API Routes:Cypher 与驱动只出现在服务端,前端只需消费 JSON,天然隔离了数据库凭据。
- 驱动单例 + 参数化 Cypher + 事务执行:util/neo4j.js 中的
getDriver()模式(配合disableLosslessIntegers免去.toNumber())与各 API 中的session.readTransaction()是官方驱动推荐的用法骨架。 - 用 map projection 直接产出 JSON:
movie {.*, actors: [...], directed: [...] }让服务端不必做二次组装,一条查询即可回传页面所需的结构化数据。
你可以基于该模板继续扩展:例如增加 POST /api/movies 写事务(session.writeTransaction)、用 REVIEWED 关系做评分聚合,或把 Cypher 换成更复杂的多跳关系查询(如"合作过的演员"),从而更充分地发挥 Neo4j 关系遍历的威力。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00