API优先的短链服务设计:RESTful规范、多语言SDK、Webhook与开发者体验

API优先的短链服务设计完整指南:RESTful API规范、多语言SDK架构、Webhook事件体系、GraphQL扩展、速率限制策略,以及打造顶级开发者体验(DX)的实战经验。

引言

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

资源层级

端点方法描述
/linksPOST创建短链
/linksGET列表查询(分页)
/links/{slug}GET获取短链详情
/links/{slug}PATCH更新短链(部分更新)
/links/{slug}DELETE删除短链
/links/{slug}/statsGET获取统计数据
/links/{slug}/qrcodeGET获取二维码
/links/{slug}/clicksGET获取点击明细(时序)
/domainsGET列出可用自定义域名
/webhooksPOST注册 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 UnauthorizedAPI 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))
        })
    }
}

四、速率限制策略

分级限制矩阵

套餐每分钟每小时每日并发
Free101005002
Starter1002000100005
Pro10002000010000020
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 设计

设计原则

  1. 语义化:方法名符合各语言习惯(Go 用 Create,Python 用 create
  2. 类型安全:强类型语言使用完整的 struct/类定义
  3. 重试策略:内置指数退避重试
  4. 流式支持:大数据量查询支持分页流

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包名版本策略
Gogithub.com/shortlink/go-sdkGo Modules
Pythonshortlink-pyPyPI
Node.js@shortlink/sdknpm
PHPshortlink/sdkPackagist
RubyshortlinkRubyGems

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可选增强,解决过度获取和关联查询
DXPlayground、详细错误、自动代码生成、Status Page

API 是产品的延伸。当开发者说"你们的 API 真好用"时,你的产品就获得了一个免费的布道者。


相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「SaaS产品设计」更多文章