发布于 2026-01-06 11 阅读
0

使用 Next.js 构建完整的 API

使用 Next.js 构建完整的 API

多年来,我一直在与各种 JavaScript 构建技术栈作斗争,最终尝试了 Next.js,并立刻爱上了它,原因很简单:它几乎没有任何预设的框架,而且它提供了一个简洁独特的构建配置,可以在后端和前端之间共享。但由于它的 API 路由底层并非 Express,我们需要找到一些变通方法才能构建一个真正的一体化应用程序。

要称得上是一个像样的 API,我们需要的远不止路由处理。我们需要独立的入口点来执行脚本和工作进程;中间件链式调用有助于保持路由安全层声明的简洁性;而且由于大多数中间件和依赖于路由的软件包都是为 Express 编写的,我们还需要一种方法来无缝集成它们。

一种解决方案是使用自定义的 Express 服务器,但这会违背框架的初衷,并失去其主要优势:自动静态优化。因此,我们尝试使用内置服务器,并逐一解决问题,以确保其流畅运行。

问题 1:中间件链

这很简单。直接使用next-connect就行了!它模拟了next()Express 的行为,并让我们重新获得了我们非常需要的 `on`  .use()、  .get()`on`、  .post().all()on` 等方法,从而无需像Next.js 文档中建议的if (req.method === 'POST') { ... }那样进行冗长的路由内方法检查

import nc from 'next-connect';

const handler = nc()
  .use(someMiddleware())
  .get((req, res) => {
    res.send('Hello world');
  })
  .post((req, res) => {
    res.json({ hello: 'world' });
  });

export default handler;
Enter fullscreen mode Exit fullscreen mode

此外,一个非常方便的功能是将其他 next-connect 实例传递给该 .use()方法,从而预定义可重用的处理器中间件:

// /path/to/handlers.js
import nc from 'next-connect';
import { acl, apiLimiter, bearerAuth } from '/path/to/middlewares';

export const baseHandler = () => nc({
  // 404 error handler
  onNoMatch: (req, res) => res.status(404).send({
    message: `API route not found: ${req.url}`,
  }),

  // 500 error handler
  onError: (err, req, res) => res.status(500).send({
    message: `Unexpected error.`,
    error: err.toString(),
  }),
});

export const secureHandler = baseHandler()
  .use(apiLimiter)
  .use(bearerAuth)
  .use(acl);


// /pages/api/index.js
import nc from 'next-connect';
import { secureHandler } from '/path/to/handlers';
const handler = nc()
  .use(secureHandler) // benefits from all above middlewares
  .get((req, res) => {
    res.send('Hello world');
  });
export default handler;
Enter fullscreen mode Exit fullscreen mode

问题 2:测试路线

在测试环境中,Next.js 服务器并未运行,这迫使我们找到一种方法来模拟请求及其解析过程。Supertest与 Express 配合使用效果很好,但它需要运行服务器才能将请求逐层传递给处理程序。也就是说,它并不一定非得是 Express。
因此,在不添加任何新依赖的情况下,我们使用原生 Node 库创建了一个裸 HTTP 服务器http,并手动应用 Next.js 内置的解析器(它被很好地封装成一个实用函数),就像这样:

import { createServer } from 'http';
import { apiResolver } from 'next/dist/next-server/server/api-utils';
import request from 'supertest';

export const testClient = (handler) => request(httpCreateServer(
  async (req, res) => {
    return apiResolver(req, res, undefined, handler);
  },
));
Enter fullscreen mode Exit fullscreen mode

在我们的测试文件中,我们只需要将处理程序传递给客户端,Supertest 即可照常运行:

import { testClient } from '/path/to/testClient';
import handler from '/pages/api/index.js';

describe('/api', () => {
  it('should deny access when not authenticated', async (done) => {
    const request = testClient(handler);
    const res = await request.get('/api');
    expect(res.status).toBe(401);
    expect(res.body.ok).toBeFalsy();
    done();
  });
});
Enter fullscreen mode Exit fullscreen mode

这样我们就无需在每次路由测试中重复设置任何东西了。非常巧妙。

问题 3:自定义入口点

入口点是需要手动运行的脚本——通常是后台进程,例如队列工作进程或迁移脚本。如果设置为独立的 Node 进程,它们将不会继承 Next.js 内置的 `import` 语法,也不会继承你可能设置的路径别名。因此,基本上,你必须手动重建 Next.js 的构建堆栈,这会使你的代码充斥package.json着 Babel 依赖项,并且需要使其与 Next.js 的最新版本保持同步。我们不希望这样做。

为了保持代码简洁,我们需要将这些代码通过 Next.js 构建管道进行处理。虽然添加自定义入口点的方法没有文档说明,但似乎可以通过以下配置实现next.config.js

const path = require('path');

module.exports = {
  webpack(config, { isServer }) {
    if (isServer) {
      return {
        ...config,
        entry() {
          return config.entry().then((entry) => ({
            ...entry,
            // your custom entry points
            worker: path.resolve(process.cwd(), 'src/worker.js'),
            run: path.resolve(process.cwd(), 'src/run.js'),
          }));
        }
      };
    }
    return config;
  },
};
Enter fullscreen mode Exit fullscreen mode

遗憾的是,它唯一的作用就是通过内部的 webpack 流程编译这些新的 JavaScript 文件,并将它们原封不动地输出到构建目录中。由于它们没有与服务器端关联,因此 Next.js 的所有功能都缺失了,包括对这种情况至关重要的环境变量。

Next.js 依赖于dotenv,因此它已被设置为我们可以重用的依赖项。然而,由于某些原因,在这些入口点顶部调用 dotenv 并不会将环境变量传递给导入的模块:

// /.env
FOO='bar';


// /src/worker.js
import dotenv from 'dotenv';
dotenv.config();

import '/path/to/module';

console.log(process.env.FOO); // outputs 'bar';


// /src/path/to/module.js
console.log(process.env.FOO); // outputs 'undefined';
Enter fullscreen mode Exit fullscreen mode

这确实很烦人。好在, dotenv-cli可以快速解决这个问题,它解析 .env文件的方式与 Next.js 相同。我们只需要在脚本命令前加上前缀即可package.json

"worker": "dotenv -c -- node .next/server/worker.js",
Enter fullscreen mode Exit fullscreen mode

请注意,它会调用构建文件夹中的脚本。您需要运行 `next dev` 或之前运行过 `next build`。考虑到将它们保留在 Next.js 构建堆栈中带来的好处,这只是一个小小的代价。

问题 4:基于快递的包裹

Next-connect 已经默认兼容一些 Express 包,例如我常用的用于检查请求参数的express-validator 。这是因为它们本质上就是中间件函数。

这些函数中有些依赖于 Express 特有的属性,例如express-acl。通常情况下,当遇到缺少的属性时,它们会抛出异常。仔细查看错误信息和包源代码,就能找到问题所在,并使用处理程序包装器来修复它:

import acl from 'express-acl';

acl.config({
  baseUrl: '/api',
  filename: 'acl.json',
  path: '/path/to/config/folder',
  denyCallback: (res) => res.status(403).json({
    ok: false,
    message: 'You are not authorized to access this resource',
  }),
});

export const aclMiddleware = (req, res, next) => {
  req.originalUrl = req.url; // Express-specific property required by express-acl
  return acl.authorize(req, res, next);
};
Enter fullscreen mode Exit fullscreen mode

因此,最大的挑战在于当软件包深度依赖 Express 时,例如 Express 会创建路由或应用程序定义。监控接口(如bull-board)就是这种情况。当我们找不到独立的替代方案时,唯一的办法就是找到一种方法来模拟整个 Express 应用程序。以下是解决方法:

import Queue from 'bull';
import { setQueues, BullAdapter, router } from 'bull-board';
import nc from 'next-connect';

setQueues([
  new BullAdapter(new Queue('main')),
]);

// tell Express app to prefix all paths
router.use('/api/monitoring', router._router);

// Forward Next.js request to Express app
const handler = nc();
handler.use((req, res, next) => {
  // manually execute Express route
  return router._router.handle(req, res, next);
});

export default handler;
Enter fullscreen mode Exit fullscreen mode

这里有几点需要注意:

  • 该文件应该位于该目录下,/pages/api因为 Next.js 只识别该目录下的服务器端路由。
  • 为了让 Express 处理包中声明的所有子路由,我们需要在 Next.js 路由上创建一个捕获所有路由的路由。这可以通过/pages/api/monitoring/[[...path]].js按照其文档中指定的名称命名路由文件来实现(将“monitoring”替换为您喜欢的任何名称)。
  • 在这个特定例子中,bull-board 以令人困惑的名称 router 暴露了一个完整的 Express 实例。这就是为什么我们需要router._router.handle()手动调用来执行路由处理程序。如果通过阅读源代码发现它是一个express.Router实例,则可以直接调用router.handle()
  • 我们还需要告诉 Express,它整个应用的根 URL 就是我们调用它的路由。我们只需app.use('/base/url', router)像往常一样定义它即可。请注意 `<base>`express和 ` express.Router<base>` 实例之间的区别。
  • 最后,由于我们传递的是完整的 Response 对象,Express 会处理响应部分。我们无需代表它发送响应头。

我之所以不采用这种技巧将整个 API 转发到模拟的 Express 应用,是因为我不知道这会对性能产生怎样的影响,而且最重要的是,我宁愿尊重 Next.js 的自然模式,以免让其他开发者感到困惑。


还不错,不是吗?最终我们得到了一个功能齐全的服务器,只是用一些占用空间极小的补丁来弥补了它的不足之处。我仍然希望 Next.js 的核心功能能够包含所有这些特性,但我很高兴我们通过这些变通方案并没有让它变得面目全非。考虑到 JavaScript 目前的状况,Next.js 很可能成为最终的全栈框架。

PS:我没有详细讲解会话和用户身份验证的设置,因为这些问题现在都已解决,您几乎可以像往常一样让所有功能正常运行。不过,我建议您了解一下next-sessionNextAuth.js

文章来源:https://dev.to/noclat/build-a-full-api-with-next-js-1ke