引言
API 是 SaaS 产品的第二界面,开发者体验(DX)就是产品体验。
当短链服务从"网页工具"进化为"基础设施",API 就成了核心交付界面。你的客户可能是一个电商 SaaS(自动为每件商品生成短链)、一个营销工具(批量创建 UTM 链接)、或一个社交平台(为每个分享动态生成短链)。他们不会登录你的后台,而是通过代码与你的服务对话。
本文从设计、实现到运维,系统讲解如何打造一流的短链 API 体验。
一、API 优先设计原则
为什么 API First?
| 优势 | 说明 |
|---|---|
| 多平台一致性 | Web、移动端、第三方集成共用同一套 API |
| 自动化集成 | CI/CD 流水线自动创建短链,无需人工介入 |
| 规模化增长 | 大客户(年调用千万次)的唯一接入方式 |
| 生态构建 | 第三方开发者基于你的 API 构建工具和应用 |
API First 设计宣言
1. API 设计先于 UI 开发
2. API 契约(OpenAPI)是唯一的真相源
3. 所有产品功能必须暴露为 API
4. 破坏性变更 = 新版本( SemVer )
5. 文档与代码同步,示例可运行
二、RESTful API 规范
基础 URI 设计
https://api.shortlink.pro/v1
资源层级
| 端点 | 方法 | 描述 |
|---|---|---|
/links | POST | 创建短链 |
/links | GET | 列表查询(分页) |
/links/{slug} | GET | 获取短链详情 |
/links/{slug} | PATCH | 更新短链(部分更新) |
/links/{slug} | DELETE | 删除短链 |
/links/{slug}/stats | GET | 获取统计数据 |
/links/{slug}/qrcode | GET | 获取二维码 |
/links/{slug}/clicks | GET | 获取点击明细(时序) |
/domains | GET | 列出可用自定义域名 |
/webhooks | POST | 注册 Webhook |
/webhooks/{id} | DELETE | 注销 Webhook |
请求/响应规范
创建短链
Request:
POST /v1/links HTTP/1.1
Host: api.shortlink.pro
Authorization: Bearer slk_xxxxxxxxxxxx
Content-Type: application/json
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{
"target_url": "https://www.example.com/products/winter-sale-2026?category=coats",
"custom_slug": "winter26",
"domain": "go.yourbrand.com",
"title": "冬季促销活动页",
"tags": ["campaign", "winter", "wechat"],
"expires_at": "2026-03-01T00:00:00Z",
"password": null,
"utm_source": "newsletter",
"utm_medium": "email",
"utm_campaign": "spring_sale",
"retargeting_pixel": {
"facebook": "1234567890",
"google": "AW-123456789"
}
}
Response (201 Created):
{
"id": "link_xxxxxxxx",
"slug": "winter26",
"short_url": "https://go.yourbrand.com/winter26",
"target_url": "https://www.example.com/products/winter-sale-2026?category=coats",
"domain": "go.yourbrand.com",
"title": "冬季促销活动页",
"tags": ["campaign", "winter", "wechat"],
"created_at": "2026-01-15T08:30:00Z",
"expires_at": "2026-03-01T00:00:00Z",
"status": "active",
"clicks": 0,
"qr_code_url": "https://api.shortlink.pro/v1/links/winter26/qrcode",
"_links": {
"self": "https://api.shortlink.pro/v1/links/winter26",
"stats": "https://api.shortlink.pro/v1/links/winter26/stats",
"clicks": "https://api.shortlink.pro/v1/links/winter26/clicks"
}
}
错误响应规范
{
"error": {
"code": "SLUG_ALREADY_EXISTS",
"message": "自定义短码 'winter26' 已被使用",
"target": "custom_slug",
"details": [
{
"code": "DUPLICATE_VALUE",
"message": "该短码在同一域名下已存在"
}
],
"request_id": "req_abc123def456",
"documentation_url": "https://docs.shortlink.pro/errors/SLUG_ALREADY_EXISTS"
}
}
HTTP 状态码使用
| 状态码 | 场景 |
|---|---|
| 200 OK | 成功响应(GET, PATCH) |
| 201 Created | 创建成功(POST) |
| 204 No Content | 删除成功(DELETE) |
| 400 Bad Request | 请求格式错误或参数校验失败 |
| 401 Unauthorized | API Key 缺失或无效 |
| 403 Forbidden | 权限不足(如无权访问该链接) |
| 404 Not Found | 资源不存在 |
| 409 Conflict | 资源冲突(如 slug 已占用) |
| 422 Unprocessable | 业务规则验证失败 |
| 429 Too Many Requests | 速率限制触发 |
| 500 Internal Error | 服务器内部错误(附带 request_id) |
三、认证与授权
API Key 体系
格式: slk_<prefix>_<random>
示例: slk_live_xxxxxxxxxxxx
slk_test_xxxxxxxxxxxx
| 前缀 | 用途 | 限制 |
|---|---|---|
live | 生产环境,计入账单 | 按套餐的速率限制 |
test | 测试环境,不计费 | 100次/小时,无真实跳转 |
readonly | 仅统计查询 | 不可创建/修改 |
OAuth 2.0(第三方应用集成)
+--------+ +---------------+
| │--(A)- Authorization Request ->│ Resource |
| │ │ Owner |
| │<-(B)-- Authorization Grant ---│ |
| │ +---------------+
| │
| │--(C)-- Authorization Grant -->│ Authorization |
| Client │ │ Server |
| │<-(D)----- Access Token -------│ |
| │ +---------------+
| │
| │--(E)----- Access Token ------>| Resource |
| │ │ Server |
| │<-(F)--- Protected Resource ---│ |
+--------+ +---------------+
Go 实现:API Key 中间件
package middleware
import (
"context"
"net/http"
"strings"
"time"
)
func APIKeyAuth(service *auth.Service) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 1. 提取 Key
authHeader := r.Header.Get("Authorization")
var apiKey string
if strings.HasPrefix(authHeader, "Bearer ") {
apiKey = strings.TrimPrefix(authHeader, "Bearer ")
} else {
apiKey = r.URL.Query().Get("api_key")
}
if apiKey == "" {
respondError(w, http.StatusUnauthorized, "MISSING_API_KEY", "API Key 不能为空")
return
}
// 2. 验证 Key
keyInfo, err := service.ValidateKey(r.Context(), apiKey)
if err != nil {
respondError(w, http.StatusUnauthorized, "INVALID_API_KEY", "API Key 无效或已撤销")
return
}
// 3. 检查速率限制
allowed, resetAt, err := service.CheckRateLimit(r.Context(), keyInfo.ID, keyInfo.Tier)
if err != nil {
respondError(w, http.StatusInternalServerError, "RATE_LIMIT_ERROR", "速率限制检查失败")
return
}
if !allowed {
w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", keyInfo.Tier.Limits.RequestsPerMinute))
w.Header().Set("X-RateLimit-Remaining", "0")
w.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", resetAt.Unix()))
respondError(w, http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED", "请求过于频繁,请稍后重试")
return
}
// 4. 注入上下文
ctx := context.WithValue(r.Context(), "api_key", keyInfo)
ctx = context.WithValue(ctx, "request_id", generateRequestID())
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
四、速率限制策略
分级限制矩阵
| 套餐 | 每分钟 | 每小时 | 每日 | 并发 |
|---|---|---|---|---|
| Free | 10 | 100 | 500 | 2 |
| Starter | 100 | 2000 | 10000 | 5 |
| Pro | 1000 | 20000 | 100000 | 20 |
| Enterprise | 自定义 | 自定义 | 自定义 | 自定义 |
响应头约定
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1704067200
X-RateLimit-Retry-After: 60
实现:Redis + 滑动窗口
package ratelimit
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type SlidingWindow struct {
client *redis.Client
}
func (sw *SlidingWindow) Allow(ctx context.Context, key string, limit int, window time.Duration) (bool, time.Time, error) {
now := time.Now()
windowStart := now.Add(-window)
redisKey := fmt.Sprintf("ratelimit:%s", key)
pipe := sw.client.Pipeline()
pipe.ZRemRangeByScore(ctx, redisKey, "0", fmt.Sprintf("%d", windowStart.UnixMilli()))
pipe.ZCard(ctx, redisKey)
pipe.ZAdd(ctx, redisKey, redis.Z{Score: float64(now.UnixMilli()), Member: now.UnixNano()})
pipe.Expire(ctx, redisKey, window)
results, err := pipe.Exec(ctx)
if err != nil {
return false, now, err
}
currentCount := results[1].(*redis.IntCmd).Val()
if int(currentCount) >= limit {
// 获取最早的一条记录,计算下一次可用时间
oldest, _ := sw.client.ZRangeWithScores(ctx, redisKey, 0, 0).Result()
if len(oldest) > 0 {
nextWindow := time.UnixMilli(int64(oldest[0].Score)).Add(window)
return false, nextWindow, nil
}
return false, now.Add(window), nil
}
return true, now, nil
}
五、Webhook 事件体系
事件类型
| 事件名 | 触发时机 | 适用场景 |
|---|---|---|
link.created | 短链创建成功 | 同步到内部系统 |
link.clicked | 短链被点击 | 实时营销触发 |
link.updated | 短链被修改 | 更新缓存 |
link.deleted | 短链被删除 | 清理关联数据 |
link.expired | 短链过期 | 归档处理 |
link.threshold.hit | 点击数达到阈值 | 告警/自动扩容 |
domain.verified | 自定义域名验证通过 | 启用域名 |
usage.quota.warning | 用量接近上限 | 升级提醒 |
usage.quota.exceeded | 用量超出上限 | 服务降级通知 |
Webhook Payload 示例
{
"event": "link.clicked",
"timestamp": "2026-01-15T14:30:00Z",
"request_id": "req_xyz789",
"data": {
"link": {
"id": "link_xxxxxxxx",
"slug": "winter26",
"short_url": "https://go.yourbrand.com/winter26",
"target_url": "https://www.example.com/products/winter-sale-2026"
},
"click": {
"timestamp": "2026-01-15T14:30:00Z",
"ip_hash": "sha256:abc123...",
"country_code": "CN",
"city": "上海",
"device_type": "mobile",
"os": "iOS",
"browser": "Safari",
"referrer": "https://weixin.qq.com/",
"utm_source": "newsletter",
"utm_medium": "email"
},
"totals": {
"clicks": 1234,
"unique_visitors": 987
}
}
}
Webhook 安全:签名验证
// 客户端验证签名示例(Go)
func verifyWebhookSignature(payload []byte, signature, secret string) bool {
// 提取时间戳和签名部分
parts := strings.Split(signature, ",")
if len(parts) != 2 {
return false
}
ts := parts[0]
sig := parts[1]
// 防重放攻击:时间戳应在5分钟内
timestamp, _ := strconv.ParseInt(strings.TrimPrefix(ts, "t="), 10, 64)
if time.Since(time.Unix(timestamp, 0)) > 5*time.Minute {
return false
}
// HMAC-SHA256 验证
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(fmt.Sprintf("%d.%s", timestamp, payload)))
expectedSig := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sig), []byte(expectedSig))
}
六、多语言 SDK 设计
设计原则
- 语义化:方法名符合各语言习惯(Go 用
Create,Python 用create) - 类型安全:强类型语言使用完整的 struct/类定义
- 重试策略:内置指数退避重试
- 流式支持:大数据量查询支持分页流
Go SDK 示例
package shortlink
import (
"context"
"net/http"
"time"
)
// Client SDK 客户端
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
}
func NewClient(apiKey string) *Client {
return &Client{
apiKey: apiKey,
baseURL: "https://api.shortlink.pro/v1",
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
func (c *Client) CreateLink(ctx context.Context, req CreateLinkRequest) (*Link, error) {
return doRequest[Link](ctx, c, http.MethodPost, "/links", req)
}
func (c *Client) GetLink(ctx context.Context, slug string) (*Link, error) {
return doRequest[Link](ctx, c, http.MethodGet, "/links/"+slug, nil)
}
func (c *Client) DeleteLink(ctx context.Context, slug string) error {
_, err := doRequest[any](ctx, c, http.MethodDelete, "/links/"+slug, nil)
return err
}
func (c *Client) ListLinks(ctx context.Context, opts ListOptions) (*PaginatedResult[Link], error) {
params := url.Values{}
if opts.Limit > 0 {
params.Set("limit", strconv.Itoa(opts.Limit))
}
if opts.Offset > 0 {
params.Set("offset", strconv.Itoa(opts.Offset))
}
if opts.Tag != "" {
params.Set("tag", opts.Tag)
}
query := "/links?" + params.Encode()
return doRequest[PaginatedResult[Link]](ctx, c, http.MethodGet, query, nil)
}
Python SDK 示例
# shortlink/client.py
import requests
from typing import Optional, List
from dataclasses import dataclass
from urllib.parse import urljoin
@dataclass
class Link:
id: str
slug: str
short_url: str
target_url: str
clicks: int
status: str
created_at: str
class Client:
def __init__(self, api_key: str, base_url: str = "https://api.shortlink.pro/v1"):
self.api_key = api_key
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
})
def create_link(self, target_url: str, **kwargs) -> Link:
"""创建短链
Args:
target_url: 目标长链接
custom_slug: 自定义短码
title: 链接标题
tags: 标签列表
expires_at: 过期时间 (ISO 8601)
Returns:
Link 对象
"""
payload = {"target_url": target_url, **kwargs}
resp = self.session.post(
f"{self.base_url}/links",
json=payload
)
resp.raise_for_status()
return Link(**resp.json())
def get_link(self, slug: str) -> Link:
resp = self.session.get(f"{self.base_url}/links/{slug}")
resp.raise_for_status()
return Link(**resp.json())
def shorten(self, url: str) -> str:
"""极简模式:传入长链接,返回短链接"""
link = self.create_link(url)
return link.short_url
JavaScript/TypeScript SDK
// src/client.ts
export class ShortlinkClient {
private apiKey: string;
private baseURL: string;
constructor(apiKey: string, options?: { baseURL?: string }) {
this.apiKey = apiKey;
this.baseURL = options?.baseURL ?? 'https://api.shortlink.pro/v1';
}
async createLink(request: CreateLinkRequest): Promise<Link> {
const response = await fetch(`${this.baseURL}/links`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
const error = await response.json();
throw new APIError(error.error.code, error.error.message, response.status);
}
return response.json();
}
async shorten(url: string): Promise<string> {
const link = await this.createLink({ target_url: url });
return link.short_url;
}
// 分页迭代器
async *listLinks(options?: ListOptions): AsyncGenerator<Link> {
let offset = 0;
const limit = options?.limit ?? 100;
while (true) {
const result = await this.request<PaginatedResult<Link>>(
`/links?limit=${limit}&offset=${offset}`
);
for (const link of result.data) {
yield link;
}
if (!result.has_more) break;
offset += limit;
}
}
}
七、GraphQL 扩展(可选增强)
为什么加 GraphQL?
REST 在简单场景中优秀,但客户端常面临:
- 过度获取:获取链接列表时不需要完整的点击统计
- 多次请求:需要链接 + 统计 + 域名状态,调 3 次 API
- 关联查询困难:“获取点击量前10的链接及其域名信息”
Schema 设计
type Link {
id: ID!
slug: String!
shortUrl: String!
targetUrl: String!
title: String
tags: [String!]
status: LinkStatus!
createdAt: DateTime!
expiresAt: DateTime
clicks: Int!
stats: LinkStats
domain: Domain
qrcode(size: Int = 256): String
}
type LinkStats {
totalClicks: Int!
uniqueVisitors: Int!
countries: [CountryStat!]!
devices: [DeviceStat!]!
dailyClicks(days: Int = 30): [DailyClick!]!
}
type Query {
link(slug: String!): Link
links(
filter: LinkFilter
orderBy: LinkOrderBy
first: Int = 20
after: String
): LinkConnection!
# 聚合查询
topLinks(
period: TimePeriod!
limit: Int = 10
): [Link!]!
}
type Mutation {
createLink(input: CreateLinkInput!): Link!
updateLink(slug: String!, input: UpdateLinkInput!): Link!
deleteLink(slug: String!): Boolean!
bulkCreateLinks(inputs: [CreateLinkInput!]!): [Link!]!
}
查询示例
# 一次请求获取链接列表 + 统计 + 域名
query DashboardQuery {
links(filter: { status: ACTIVE }, first: 10, orderBy: { field: CLICKS, direction: DESC }) {
edges {
node {
slug
shortUrl
targetUrl
clicks
stats {
uniqueVisitors
countries(limit: 5) {
code
name
count
}
}
domain {
hostname
status
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
八、开发者体验(DX)优化
1. 即用的 API Playground
提供交互式的 API 控制台(类似 Stripe API Reference),开发者可以:
- 直接在线发送请求
- 查看实时响应
- 一键复制 curl/Go/Python 代码
2. 详细的错误信息
{
"error": {
"code": "INVALID_TARGET_URL",
"message": "目标链接格式无效",
"target": "target_url",
"suggestion": "请确保链接包含协议(https://)且为有效URL",
"example": "https://www.example.com/page",
"documentation_url": "https://docs.shortlink.pro/errors/INVALID_TARGET_URL",
"request_id": "req_abc123"
}
}
3. SDK 版本管理
| SDK | 包名 | 版本策略 |
|---|---|---|
| Go | github.com/shortlink/go-sdk | Go Modules |
| Python | shortlink-py | PyPI |
| Node.js | @shortlink/sdk | npm |
| PHP | shortlink/sdk | Packagist |
| Ruby | shortlink | RubyGems |
4. Postman/Insomnia Collection
提供一键导入的 API Collection:
# Postman
https://api.shortlink.pro/docs/postman-collection.json
# OpenAPI
https://api.shortlink.pro/docs/openapi.yaml
5. 变更日志与迁移指南
## v1.2.0 (2026-02-01)
### 新增
- `link.threshold.hit` Webhook 事件
- 批量创建 API (`POST /v1/links/bulk`),单次最多 100 条
- GraphQL 支持设备统计查询
### 变更
- `GET /v1/links/{slug}/stats` 响应中的 `unique_clicks` 字段更名为 `unique_visitors`
- 旧字段仍保留,将在 v2.0.0 中移除
### 废弃
- `POST /v1/links/batch`(请迁移至 `/bulk`)
6. Status Page + API Health
GET /v1/health
{
"status": "healthy",
"version": "1.2.3",
"timestamp": "2026-01-15T14:30:00Z",
"services": {
"database": "healthy",
"redis": "healthy",
"domain_resolver": "degraded",
"webhook_queue": "healthy"
},
"metrics": {
"requests_per_minute": 15234,
"average_response_ms": 12.3,
"error_rate": 0.001
}
}
九、总结
一流的短链 API 设计要点:
| 维度 | 关键决策 |
|---|---|
| RESTful 设计 | 资源为中心,语义化 HTTP 方法,统一的错误响应 |
| 认证授权 | API Key + OAuth 2.0 双轨,前缀区分环境 |
| 速率限制 | Redis 滑动窗口,响应头透明,分级套餐 |
| Webhook | 事件丰富,签名验证,重试机制 |
| SDK | 多语言覆盖,类型安全,内置重试 |
| GraphQL | 可选增强,解决过度获取和关联查询 |
| DX | Playground、详细错误、自动代码生成、Status Page |
API 是产品的延伸。当开发者说"你们的 API 真好用"时,你的产品就获得了一个免费的布道者。
相关阅读
- 短链 SaaS 产品全景 — 本专题总览
- 盈利模型深度拆解 — API 定价与计费
- MarTech 生态集成 — Webhook 驱动的营销自动化
- 白标平台构建方案 — 多租户 API 设计
- 高并发短链架构设计 — API 层的性能优化
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。