快递查询API

← 技术博客

用 Node.js 对接快递查询接口:原生 fetch 与 Express 代理

2026-07-26 · 阅读约 7 分钟

给后台、电商系统或小程序后端加一个「查快递」能力,用 Node.js 对接现成的快递查询接口是最省事的路子。 Node 18 起内置了全局 fetch,连 axios 都不用装。本文从最小请求讲到批量查询、TypeScript 类型, 再到最关键的一步——用 Express 包一层代理端点,把密钥牢牢留在服务端。

一、为什么 Node 18+ 不用再装 axios

过去在 Node 里发 HTTP 请求要装 axios 或 node-fetch;从 Node 18 起,fetchHeadersResponse 都是全局可用的标准 API,写法和浏览器一致。除 Express 那节外,下面的示例只依赖 Node 18+ 自带能力。 先把密钥放进环境变量:

# .env —— 放在服务端,切勿提交仓库、切勿打包进前端
KUAIDI_PK=pk_xxx
KUAIDI_SK=sk_xxx

认证格式是 Bearer API_KEY:SECRET_KEY——把 pk_ 开头的 API Key 与 sk_ 开头的 Secret Key 用英文冒号拼接成一个字符串。

二、最小可用示例

查询接口是 POST /v1/tracking/trace,请求体里放一个 items 数组。先查单个运单号:

// chaxun.mjs —— Node 18+ 自带全局 fetch,无需安装 axios
const AUTH = 'Bearer ' + process.env.KUAIDI_PK + ':' + process.env.KUAIDI_SK;

async function chaxun(courierCode, trackingNumber) {
  const res = await fetch('https://api.kuaidichaxunapi.com/v1/tracking/trace', {
    method: 'POST',
    headers: {
      'Authorization': AUTH,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ items: [{ courierCode, trackingNumber }] }),
  });
  const json = await res.json();
  return json.data.results[0];
}

const r = await chaxun('cainiao', 'CGD0000123456');
console.log(r.success ? r.data.deliveryStatusText : r.error.code);

r.successtrue 时读 r.data,为 false 时读 r.errordeliveryStatusText 是已归一好的中文状态,可直接展示。

三、批量查询(一次最多 10 个)

items 一次最多放 10 个运单号。返回里除了 results,还有一个 summary, 汇总了 total(总数)、successful(成功)、failed(失败)、billable(实际计费条数):

// 一次最多 10 个运单号;成功条目计费,COURIER_PREPARING 与参数错误不计费
async function piliang(items) {
  const res = await fetch('https://api.kuaidichaxunapi.com/v1/tracking/trace', {
    method: 'POST',
    headers: { 'Authorization': AUTH, 'Content-Type': 'application/json' },
    body: JSON.stringify({ items }),
  });
  const json = await res.json();
  return json.data; // { results, summary }
}

const data = await piliang([
  { courierCode: 'cainiao', trackingNumber: 'CGD0000123456' },
  { courierCode: 'zto', trackingNumber: 'ZT0000000001' },
]);
console.log(data.summary); // { total, successful, failed, billable }

计费只按成功查询计算:COURIER_PREPARING(该公司查询功能开发中)与参数类错误都不消耗额度, 看 summary.billable 就知道这批实际计费了几条。

四、加上 TypeScript 类型

用 TypeScript 的话,给返回值定义类型能少踩不少字段拼写的坑。成功与失败是两种结构, 用可辨识联合(discriminated union)最合适:

// 返回类型(简化版,够日常使用);成功与失败是两种结构
interface Progress {
  dateTime: string;
  location: string;
  status: string;
  statusCode: string;
  description: string;
}

interface TraceOk {
  success: true;
  data: {
    trackingNumber: string;
    courierName: string;
    deliveryStatus: string;
    deliveryStatusText: string;
    isDelivered: boolean;
    dateDelivered: string | null;
    progresses: Progress[];
  };
}

interface TraceErr {
  success: false;
  error: { code: string; message: string; billable: boolean };
}

type TraceResult = TraceOk | TraceErr;

判断 success 之后,TypeScript 会自动把类型收窄到 TraceOkTraceErr, 访问 dataerror 都不会报错。

五、关键:用 Express 代理,密钥只留在服务端

Secret Key 一旦打包进前端(网页 JS、小程序包),任何人打开开发者工具或抓包都能读到,然后拿你的额度随便刷。 正确做法是:前端只调用你自己的后端,后端再带着密钥去请求快递 API。

// server.mjs —— npm i express;密钥只存在于服务端环境变量
import express from 'express';
const app = express();
app.use(express.json());

const AUTH = 'Bearer ' + process.env.KUAIDI_PK + ':' + process.env.KUAIDI_SK;

// 前端只调用这个端点,永远看不到 pk_ / sk_
app.post('/api/kuaidi', async (req, res) => {
  const items = (req.body.items || []).slice(0, 10);
  const upstream = await fetch('https://api.kuaidichaxunapi.com/v1/tracking/trace', {
    method: 'POST',
    headers: { 'Authorization': AUTH, 'Content-Type': 'application/json' },
    body: JSON.stringify({ items }),
  });
  const json = await upstream.json();
  res.json(json.data); // 只透传 data,不外泄 demoUsage 等信息
});

app.listen(3000);

前端这样调用,全程接触不到 pk_ / sk_

// 浏览器 / 前端:只知道自家后端地址,从不接触密钥
const res = await fetch('/api/kuaidi', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ items: [{ courierCode: 'cainiao', trackingNumber: 'CGD0000123456' }] }),
});
const { results } = await res.json();

六、健壮性:网络异常与逐条失败分开处理

两类错误要分开:一类是网络层面(超时、5xx),用 try/catch 兜住; 另一类是单条运单查询失败,藏在 results 里逐条判断 success。 别让一条查不到就让整个请求崩掉:

// 把网络异常与逐条业务失败分开处理
async function anquanChaxun(items) {
  let json;
  try {
    const res = await fetch('https://api.kuaidichaxunapi.com/v1/tracking/trace', {
      method: 'POST',
      headers: { 'Authorization': AUTH, 'Content-Type': 'application/json' },
      body: JSON.stringify({ items }),
    });
    if (!res.ok) throw new Error('HTTP ' + res.status);
    json = await res.json();
  } catch (e) {
    console.error('网络或服务异常:', e.message);
    return [];
  }
  return json.data.results.map((r) => {
    if (r.success) return { trackingNumber: r.data.trackingNumber, status: r.data.deliveryStatus };
    // NOT_FOUND / COURIER_PREPARING / INVALID_TRACKING_NUMBER…
    return { trackingNumber: r.error.trackingNumber, error: r.error.code };
  });
}

对接的认证、字段与错误码细节见接口文档,各家快递的编码与运单号格式见快递公司页。 想先不写代码跑一遍,可以用首页的在线演示(公开测试密钥),或直接注册拿自己的密钥。