一个新手用 Bun + Axios 调通 DeepSeek API 的实践记录

前言

最近我尝试用 Bun 写了一个调用 DeepSeek API 的小 demo。

严格来说,这不是一个复杂项目,更像是一次从 0 到 1 调通接口的练习。过程中我顺便理解了 Bun 怎么运行 TypeScript、Axios 怎么发送 POST 请求、API Key 应该怎么放到 .env 里,以及一个大模型 API 请求大概由哪些部分组成。

这篇文章主要记录整个流程,也适合和我一样刚接触 Bun、Axios、大模型 API 调用的同学参考。


这个 demo 做了什么

先用一句话概括:

使用 Bun 运行 TypeScript 代码,通过 Axios 向 DeepSeek API 发送请求,然后把大模型返回的内容打印到终端。

整体流程大概是这样:

javascript 复制代码
用户输入问题
  ↓
Axios 发送 HTTP POST 请求
  ↓
请求带着 API Key 到 DeepSeek 服务器
  ↓
DeepSeek 模型生成回答
  ↓
服务器返回 JSON 数据
  ↓
程序从 JSON 中取出回答并打印

所以这个 demo 并不是在本地运行大模型,也不是训练模型,而是调用 DeepSeek 提供好的 API 服务。


Bun 是什么

一句话:Bun 是一个新的 JavaScript 运行时,也可以看作 Node.js 的替代方案之一。

它不只是运行时,还内置了包管理、打包、测试等能力。

几个比较实用的特点:

  • 速度快:Bun 使用 Zig 编写,底层使用 JavaScriptCore,启动和安装依赖都比较快
  • 原生运行 TypeScript :不用额外安装 ts-node,可以直接运行 .ts 文件
  • 内置包管理器 :可以用 bun installbun add 管理依赖
  • 自动读取 .env :一般情况下不需要额外安装 dotenv
  • 自带测试工具 :可以用 bun test 跑测试

对于新手来说,最直观的感受就是:很多原来需要单独配置的东西,Bun 已经帮我们内置好了。


初始化项目

创建项目目录:

bash 复制代码
mkdir axios-demo
cd axios-demo
bun init

初始化过程中可以一路回车,先使用默认配置即可。

项目结构大概是这样:

bash 复制代码
axios-demo/
├── index.ts
├── package.json
├── tsconfig.json
├── .env
├── .gitignore
└── bun.lock

其中比较重要的是:

bash 复制代码
index.ts        写主要代码
package.json    记录项目依赖和脚本
tsconfig.json   TypeScript 配置
.env            存放环境变量,比如 API Key
.gitignore      配置哪些文件不提交到 Git 仓库
bun.lock        Bun 生成的依赖锁定文件

安装依赖

这个 demo 只需要安装 Axios:

csharp 复制代码
bun add axios

如果项目里还没有 Bun 的类型提示,也可以安装:

sql 复制代码
bun add -d @types/bun

Bun 会自动加载 .env 文件,所以这里不需要安装 dotenv,也不需要手动写 dotenv.config()

安装完成后,package.json 大概类似这样:

json 复制代码
{
  "name": "axios-demo",
  "module": "index.ts",
  "type": "module",
  "private": true,
  "devDependencies": {
    "@types/bun": "latest"
  },
  "peerDependencies": {
    "typescript": "^5"
  },
  "dependencies": {
    "axios": "^1.17.0"
  }
}

这里的版本号不一定完全一样,以自己实际安装出来的为准。


TypeScript 配置

Bun 初始化项目时通常会生成一份 tsconfig.json

我的配置大概如下:

json 复制代码
{
  "compilerOptions": {
    "lib": ["ESNext"],
    "target": "ESNext",
    "module": "Preserve",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "allowJs": true,
    "types": ["bun"],
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "strict": true,
    "skipLibCheck": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true
  }
}

其中我目前主要理解这三个配置:

json 复制代码
"module": "Preserve"

表示保留源码里的模块语法,不急着转换,交给 Bun 去处理。

json 复制代码
"types": ["bun"]

表示让 TypeScript 和编辑器识别 Bun 提供的内置 API。

json 复制代码
"noEmit": true

表示 TypeScript 不需要额外输出 JavaScript 文件,因为 Bun 可以直接运行 .ts 文件。


配置环境变量

在项目根目录下创建 .env 文件:

ini 复制代码
DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
DEEPSEEK_API_KEY=sk-your-api-key-here

这里有两个变量:

vbnet 复制代码
DEEPSEEK_API_URL    DeepSeek 的接口地址
DEEPSEEK_API_KEY    自己的 API Key

注意:.env 文件里存的是敏感信息,尤其是 API Key。

所以一定要在 .gitignore 里加上:

bash 复制代码
.env

否则如果不小心把 API Key 提交到 GitHub 或 Gitee,别人就可能拿你的 Key 去调用接口,甚至消耗你的额度。


编写代码

核心代码写在 index.ts 里:

javascript 复制代码
import axios from "axios";

const apiURL = process.env.DEEPSEEK_API_URL;
const apiKey = process.env.DEEPSEEK_API_KEY;

if (!apiURL) {
  throw new Error("DEEPSEEK_API_URL 没有配置");
}

if (!apiKey) {
  throw new Error("DEEPSEEK_API_KEY 没有配置");
}

async function chat() {
  try {
    const res = await axios.post(
      apiURL,
      {
        model: "deepseek-v4-flash",
        messages: [
          {
            role: "user",
            content: "你好,介绍一下 Bun",
          },
        ],
      },
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${apiKey}`,
        },
      }
    );

    console.log(res.data.choices[0].message.content);
  } catch (err: any) {
    console.error("请求失败:", err.message);

    if (err.response) {
      console.error("HTTP 状态码:", err.response.status);
      console.error("错误详情:", err.response.data);
    }
  }
}

chat();

运行:

arduino 复制代码
bun run index.ts

也可以直接:

复制代码
bun index.ts

如果配置没问题,终端里应该就能看到 DeepSeek 返回的回答。


代码拆解

一开始我只是把代码跑通了,但后来回头看,其实这段代码主要分成几部分。

1. 读取环境变量

ini 复制代码
const apiURL = process.env.DEEPSEEK_API_URL;
const apiKey = process.env.DEEPSEEK_API_KEY;

这两行代码是从 .env 文件里读取配置。

因为 Bun 会自动加载 .env,所以我们可以直接通过 process.env 拿到对应的值。

2. 判断配置是否存在

javascript 复制代码
if (!apiURL) {
  throw new Error("DEEPSEEK_API_URL 没有配置");
}

if (!apiKey) {
  throw new Error("DEEPSEEK_API_KEY 没有配置");
}

如果没有配置接口地址或者 API Key,程序就直接报错。

这样做的好处是:

问题能尽早暴露,不然请求发出去之后再失败,排查起来更麻烦。

3. 使用 axios.post 发送请求

php 复制代码
const res = await axios.post(
  apiURL,
  {
    model: "deepseek-v4-flash",
    messages: [
      {
        role: "user",
        content: "你好,介绍一下 Bun",
      },
    ],
  },
  {
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${apiKey}`,
    },
  }
);

axios.post() 这里有三个主要参数:

css 复制代码
第一个参数:请求地址
第二个参数:请求体 body
第三个参数:请求配置,比如 headers

对应到这段代码就是:

复制代码
apiURL

表示请求要发到哪里。

css 复制代码
{
  model: "deepseek-v4-flash",
  messages: [
    {
      role: "user",
      content: "你好,介绍一下 Bun",
    },
  ],
}

表示要发送给 DeepSeek 的数据。

css 复制代码
{
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${apiKey}`,
  },
}

表示请求头,告诉服务器数据格式是什么,以及我是谁。

4. 打印模型返回内容

ini 复制代码
console.log(res.data.choices[0].message.content);

DeepSeek 返回的不是一段普通字符串,而是一份 JSON 数据。

我们要从这份 JSON 里面取出模型真正回复的内容。

可以简单理解成:

css 复制代码
res
  ↓
data
  ↓
choices[0]
  ↓
message
  ↓
content

最后拿到的 content 才是模型返回的文字。


三个必知的基础知识

1. 为什么这里用 POST,而不是 GET

像 Chat Completions 这种接口,需要把 modelmessages 等数据放到请求体里发送,所以一般使用 POST。

GET 和 POST 可以简单对比一下:

维度 GET POST
常见用途 获取数据 提交数据
参数位置 通常放在 URL 上 通常放在请求体 body 里
数据量 受 URL 长度限制 更适合提交较大的 JSON
敏感信息 不适合放在 URL 里 一般放在请求头或请求体里

在这个 demo 里,messages 可能会很长,还需要传 model 等参数,所以用 POST 更合适。

需要注意的是,不是所有大模型相关接口都必须用 POST。

比如有些"查询模型列表"的接口就可能使用 GET。

所以实际开发时,还是要以接口文档为准。


2. HTTP 请求大概由哪几部分组成

一个 HTTP 请求可以先简单理解成三部分:

复制代码
请求行:请求方法 + 请求路径 + 协议版本
请求头:一些额外信息,比如数据格式、认证信息
请求体:真正提交的数据

对应到本次请求,大概是:

bash 复制代码
请求行:
POST /chat/completions HTTP/1.1

请求头:
Content-Type: application/json
Authorization: Bearer sk-xxx

请求体:
{
  "model": "deepseek-v4-flash",
  "messages": [
    {
      "role": "user",
      "content": "你好,介绍一下 Bun"
    }
  ]
}

理解这三部分以后,再去看其他 HTTP API,会清楚很多。

比如:

javascript 复制代码
接口地址放哪里?
认证信息放哪里?
JSON 数据放哪里?

这些问题就能慢慢对应起来。


3. Axios 错误处理为什么比较方便

原生 fetch 在遇到 400、500 这类 HTTP 状态码时,不一定会直接进入 catch,通常需要自己判断 response.ok

而 Axios 默认情况下,只要状态码不在 2xx 范围内,就会进入 catch

所以我们可以这样处理错误:

typescript 复制代码
try {
  const res = await axios.post(url, data, config);
  console.log(res.data);
} catch (err: any) {
  console.error("请求失败:", err.message);

  if (err.response) {
    console.error("HTTP 状态码:", err.response.status);
    console.error("错误详情:", err.response.data);
  }
}

这对于调试 API 很方便。

比如:

vbnet 复制代码
401:可能是 API Key 错了
404:可能是接口地址写错了
429:可能是请求太频繁或额度限制
500:可能是服务端内部错误

出错以后,能看到状态码和错误详情,排查起来会轻松很多。


Bun 常用命令速查

场景 Node.js / npm Bun
初始化项目 npm init bun init
安装依赖 npm install bun install
添加依赖 npm install axios bun add axios
添加开发依赖 npm install -D xxx bun add -d xxx
运行 TS 文件 npx ts-node index.ts bun index.ts
运行脚本 npm run dev bun run dev
临时执行命令 npx eslint . bunx eslint .
运行测试 npx jest bun test
加载 .env 通常需要 dotenv 自动加载

Bun 还有哪些内置能力

这次 demo 里主要用到了 Bun 运行 TypeScript 和自动加载 .env 的能力。

除此之外,Bun 还内置了一些常见功能。

下面这些代码主要是为了了解 Bun 的能力范围,不是本篇 demo 的必要代码。

javascript 复制代码
// HTTP 服务器
Bun.serve({
  port: 3000,
  fetch(req) {
    return new Response("Hello Bun!");
  },
});

// SQLite
import { Database } from "bun:sqlite";

const db = new Database("mydb.sqlite");

// 文件操作
const file = Bun.file("./data.json");
const data = await file.json();

// Shell 命令
const result = await Bun.$`ls -la`.text();

也就是说,Bun 不只是能运行 JavaScript / TypeScript,它还想把很多常用开发能力都集成进来。

对于小项目或者练习项目来说,这种"一套工具完成很多事"的体验还是挺方便的。


常见问题排查

1. 提示 API Key 没有配置

先检查 .env 文件是不是放在项目根目录。

正确位置应该是:

bash 复制代码
axios-demo/
├── index.ts
├── package.json
└── .env

再检查变量名是否和代码里一致:

ini 复制代码
DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
DEEPSEEK_API_KEY=sk-your-api-key-here

代码里也要对应:

arduino 复制代码
process.env.DEEPSEEK_API_URL
process.env.DEEPSEEK_API_KEY

2. 提示 401

一般是认证失败。

常见原因:

vbnet 复制代码
API Key 写错了
API Key 前后多了空格
Authorization 格式写错了
Key 已经过期或被禁用

请求头应该类似这样:

javascript 复制代码
Authorization: `Bearer ${apiKey}`

3. 提示模型不存在

如果返回类似"model not found"的错误,优先检查 model 字段。

vbnet 复制代码
model: "deepseek-v4-flash"

模型名称可能会随着平台调整而变化,所以实际项目里建议以官方文档或控制台显示为准。


总结

这个 demo 虽然很小,但对我来说还是挺有收获的。

它让我把下面几个知识点串起来了:

  1. 用 Bun 初始化和运行 TypeScript 项目
  2. 用 Axios 发送 HTTP POST 请求
  3. 把 API Key 放到 .env 里,而不是直接写死在代码中
  4. 理解 HTTP 请求里的请求头和请求体
  5. 知道大模型接口返回的是 JSON,需要从里面取出真正的回复内容
  6. 学会通过状态码和错误信息排查问题

最后再总结一下这次请求的核心流程:

javascript 复制代码
Bun 运行 index.ts
  ↓
读取 .env 中的 API 地址和 API Key
  ↓
Axios 发送 POST 请求
  ↓
DeepSeek 返回 JSON
  ↓
从 JSON 中取出 message.content
  ↓
打印到终端

整体来看,Bun 的上手成本确实比较低,尤其是能直接运行 TypeScript、自动加载 .env,对新手很友好。

这次只是调通了一个最小 demo,后面如果继续扩展,可以尝试把它改成一个简单的 Web 页面,做到:

复制代码
页面输入问题
  ↓
后端调用 DeepSeek API
  ↓
页面显示模型回答

这样就能从"命令行 demo"慢慢过渡到一个真正的小项目。

相关推荐
不好听6131 小时前
深入理解链表:线性数据结构的另一面
javascript·数据结构
林希_Rachel_傻希希1 小时前
学React治好了我的焦虑症,1小时速通React 前20分钟。
前端·javascript·面试
小林ixn1 小时前
从 Ajax 到异步编程:JSON 序列化、Event Loop 与 XHR 请求完全解析
javascript
丷丩3 小时前
MapLibre GL JS第47课:添加动画图标
javascript·gis·动画·mapbox·maplibre
快乐的哈士奇3 小时前
【Next.js实战①】Gmail API 按柜号检索邮件:OAuth 双 Cookie 与搜索 Fallback
开发语言·javascript·ecmascript
云水一下3 小时前
Vue.js从零到精通系列(五):全局状态管理——Pinia 核心与实践
前端·javascript·vue.js
kmblack14 小时前
javascript计算年龄
开发语言·javascript·ecmascript
Dick5074 小时前
ROS2 多机器人通用 Driver 层复盘:BaseRobotDriver 到多平台 Mock 切换实现
前端·javascript·机器人