responder

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 27, 2026 License: MIT Imports: 2 Imported by: 0

README

HTTP Responder - 前端使用文档

📋 响应数据结构

所有 API 响应都遵循统一的结构:

interface Response<T> {
  data: T | null;        // 响应数据
  error: Error | null;   // 错误信息
  meta: Meta;           // 元数据
}

interface Error {
  code: number;          // 错误码
  message: string;       // 错误消息
  details?: any;         // 详细信息(可选)
}

interface Meta {
  traceId?: string;      // 请求追踪 ID
  took?: number;         // 处理耗时(毫秒)
  pagination?: Pagination; // 分页信息
}

interface Pagination {
  page: number;          // 当前页码
  pageSize: number;      // 每页条数
  total: number;         // 总条数
  totalPages: number;    // 总页数
  hasMore: boolean;      // 是否还有更多
}

📝 成功响应示例

1. 单条数据查询

请求: GET /api/posts/1

响应:

{
  "data": {
    "id": 1,
    "title": "Hello World",
    "content": "这是文章内容",
    "createdAt": "2024-01-12T10:00:00Z"
  },
  "error": null,
  "meta": {
    "traceId": "abc-123-def",
    "took": 15
  }
}

前端处理:

const response = await fetch('/api/posts/1');
const result = await response.json();

if (result.error) {
  // 处理错误
  console.error(result.error.message);
  return;
}

// 使用数据
const post = result.data;
console.log(post.title);
2. 列表查询(带分页)

请求: GET /api/posts?page=1&pageSize=10

响应:

{
  "data": [
    { "id": 1, "title": "文章1" },
    { "id": 2, "title": "文章2" }
  ],
  "error": null,
  "meta": {
    "traceId": "xyz-456-uvw",
    "took": 23,
    "pagination": {
      "page": 1,
      "pageSize": 10,
      "total": 100,
      "totalPages": 10,
      "hasMore": true
    }
  }
}

前端处理:

const response = await fetch('/api/posts?page=1&pageSize=10');
const result = await response.json();

if (result.error) {
  console.error(result.error.message);
  return;
}

const posts = result.data; // 数组
const pagination = result.meta.pagination;

// 渲染列表
posts.forEach(post => {
  // ...
});

// 分页控件
console.log(`第 ${pagination.page}/${pagination.totalPages} 页`);
console.log(`总共 ${pagination.total} 条`);
3. 创建/更新操作

请求: POST /api/posts

响应:

{
  "data": {
    "id": 101,
    "title": "新文章",
    "createdAt": "2024-01-12T10:05:00Z"
  },
  "error": null,
  "meta": {
    "traceId": "req-789",
    "took": 45
  }
}

❌ 错误响应示例

1. 参数验证错误

响应: 400 Bad Request

{
  "data": null,
  "error": {
    "code": 4001,
    "message": "invalid request body",
    "details": {
      "title": "required field",
      "content": "min length 10 required"
    }
  },
  "meta": {
    "traceId": "err-001",
    "took": 5
  }
}
2. 资源不存在

响应: 404 Not Found

{
  "data": null,
  "error": {
    "code": 4041,
    "message": "post not found"
  },
  "meta": {
    "traceId": "err-002",
    "took": 8
  }
}
3. 服务器错误

响应: 500 Internal Server Error

{
  "data": null,
  "error": {
    "code": 5000,
    "message": "internal server error",
    "details": "database connection failed"
  },
  "meta": {
    "traceId": "err-003",
    "took": 2
  }
}

🎨 前端封装示例

通用请求封装
interface ApiResponse<T> {
  data: T | null;
  error: Error | null;
  meta: Meta;
}

interface Error {
  code: number;
  message: string;
  details?: any;
}

interface Meta {
  traceId?: string;
  took?: number;
  pagination?: Pagination;
}

class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string = '/api') {
    this.baseUrl = baseUrl;
  }

  async get<T>(path: string, params?: Record<string, any>): Promise<T> {
    const url = this.buildUrl(path, params);
    const response = await fetch(url);
    return this.handleResponse<T>(response);
  }

  async post<T>(path: string, body?: any): Promise<T> {
    const url = `${this.baseUrl}${path}`;
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body)
    });
    return this.handleResponse<T>(response);
  }

  async put<T>(path: string, body?: any): Promise<T> {
    const url = `${this.baseUrl}${path}`;
    const response = await fetch(url, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body)
    });
    return this.handleResponse<T>(response);
  }

  async delete<T>(path: string): Promise<T> {
    const url = `${this.baseUrl}${path}`;
    const response = await fetch(url, { method: 'DELETE' });
    return this.handleResponse<T>(response);
  }

  private async handleResponse<T>(response: Response): Promise<T> {
    const result: ApiResponse<T> = await response.json();

    if (result.error) {
      // 可以在这里统一处理错误提示
      throw new ApiError(result.error.message, result.error.code, result.error.details);
    }

    return result.data as T;
  }

  private buildUrl(path: string, params?: Record<string, any>): string {
    if (!params) return `${this.baseUrl}${path}`;

    const searchParams = new URLSearchParams(params);
    return `${this.baseUrl}${path}?${searchParams.toString()}`;
  }
}

class ApiError extends Error {
  constructor(
    message: string,
    public code: number,
    public details?: any
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

// 使用示例
const api = new ApiClient('http://localhost:8080/api');

// 查询单条
const post = await api.get<Post>('/posts/1');

// 查询列表
const posts = await api.get<Post[]>('/posts', { page: 1, pageSize: 10 });

// 创建
const newPost = await api.post<Post>('/posts', { title: 'New', content: '...' });

// 更新
const updated = await api.put<Post>('/posts/1', { title: 'Updated' });

// 删除
await api.delete<void>('/posts/1');
React Hook 示例
import { useState, useEffect } from 'react';

interface UseApiResponse<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => Promise<void>;
}

function useApi<T>(path: string): UseApiResponse<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const fetch = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(path);
      const result = await response.json();

      if (result.error) {
        setError(result.error);
        setData(null);
      } else {
        setData(result.data);
      }
    } catch (err) {
      setError({ code: 5000, message: 'Network error' });
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetch();
  }, [path]);

  return { data, loading, error, refetch: fetch };
}

// 使用
function PostDetail({ id }: { id: number }) {
  const { data: post, loading, error } = useApi<Post>(`/api/posts/${id}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!post) return <div>Not found</div>;

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}
分页组件示例
interface PaginationProps {
  pagination: Pagination;
  onPageChange: (page: number) => void;
}

function Pagination({ pagination, onPageChange }: PaginationProps) {
  const { page, totalPages, hasMore } = pagination;

  return (
    <div className="pagination">
      <button
        disabled={page === 1}
        onClick={() => onPageChange(page - 1)}
      >
        上一页
      </button>

      <span>第 {page} / {totalPages} 页</span>

      <button
        disabled={!hasMore}
        onClick={() => onPageChange(page + 1)}
      >
        下一页
      </button>
    </div>
  );
}

// 使用
function PostList() {
  const [page, setPage] = useState(1);
  const [posts, setPosts] = useState<Post[]>([]);
  const [pagination, setPagination] = useState<Pagination | null>(null);

  useEffect(() => {
    fetch(`/api/posts?page=${page}&pageSize=10`)
      .then(res => res.json())
      .then(result => {
        if (!result.error) {
          setPosts(result.data);
          setPagination(result.meta.pagination);
        }
      });
  }, [page]);

  return (
    <div>
      <PostGrid posts={posts} />
      {pagination && (
        <Pagination
          pagination={pagination}
          onPageChange={setPage}
        />
      )}
    </div>
  );
}

🔍 调试技巧

1. 使用 TraceId 追踪请求
// 在开发环境中显示 traceId
fetch('/api/posts/1')
  .then(res => res.json())
  .then(result => {
    console.log('Trace ID:', result.meta.traceId);
    console.log('Processing time:', result.meta.took, 'ms');
  });
2. 统一错误处理
function handleApiError(error: ApiError) {
  switch (error.code) {
    case 4001:
      // 参数验证错误
      console.error('参数错误:', error.details);
      break;
    case 4041:
      // 资源不存在
      console.error('资源不存在');
      break;
    case 5000:
      // 服务器错误
      console.error('服务器错误:', error.message);
      break;
    default:
      console.error('未知错误:', error);
  }
}

📌 注意事项

  1. 所有响应都有 dataerrormeta 三个字段

    • 成功时:data 有值,errornull
    • 失败时:error 有值,datanull
  2. 分页查询必须检查 meta.pagination

    • 列表接口返回分页信息
    • 单条查询没有分页信息
  3. 错误码规范

    • 4xxx: 客户端错误(4001: 验证失败, 4041: 未找到)
    • 5xxx: 服务器错误(5000: 内部错误)
  4. 可选字段

    • error.details: 错误详细信息
    • meta.traceId: 请求追踪 ID
    • meta.took: 处理耗时

Documentation

Index

Constants

View Source
const (
	// 4xxx - 客户端错误
	ErrCodeBadRequest        = 4000 // 请求格式错误
	ErrCodeBindFailed        = 4001 // 参数绑定错误
	ErrCodeValidationFailed  = 4002 // 数据验证失败
	ErrCodeNotFound          = 4003 // 资源不存在
	ErrCodeRouteNotFound     = 4004 // 路由不存在
	ErrCodeForbidden         = 4005 // 权限不足
	ErrCodeUnauthorized      = 4006 // 认证失败
	ErrCodeDuplicate         = 4007 // 资源已存在
	ErrCodeConflict          = 4008 // 数据冲突
	ErrCodeTooManyRequests   = 4009 // 请求过于频繁
	ErrCodeRateLimitExceeded = 4009 // 速率限制超出 (别名)

	// 5xxx - 服务端错误
	ErrCodeInternalServer  = 5000 // 内部服务器错误
	ErrCodeDatabase        = 5001 // 数据库错误
	ErrCodeBusinessLogic   = 5002 // 业务逻辑错误
	ErrCodeFileUpload      = 5003 // 文件上传失败
	ErrCodeStorageService  = 5004 // 存储服务错误
	ErrCodeExternalService = 5005 // 外部服务错误
	ErrCodeTimeout         = 5006 // 请求超时
)

HTTP 状态码相关的错误码

Variables

View Source
var (
	ErrBadRequest       = NewError(ErrCodeBadRequest, "")
	ErrBindFailed       = NewError(ErrCodeBindFailed, "")
	ErrValidationFailed = NewError(ErrCodeValidationFailed, "")
	ErrNotFound         = NewError(ErrCodeNotFound, "")
	ErrRouteNotFound    = NewError(ErrCodeRouteNotFound, "")
	ErrForbidden        = NewError(ErrCodeForbidden, "")
	ErrUnauthorized     = NewError(ErrCodeUnauthorized, "")
	ErrDuplicate        = NewError(ErrCodeDuplicate, "")
	ErrConflict         = NewError(ErrCodeConflict, "")
	ErrTooManyRequests  = NewError(ErrCodeTooManyRequests, "")
	ErrInternalServer   = NewError(ErrCodeInternalServer, "")
	ErrDatabase         = NewError(ErrCodeDatabase, "")
	ErrBusinessLogic    = NewError(ErrCodeBusinessLogic, "")
	ErrFileUpload       = NewError(ErrCodeFileUpload, "")
	ErrStorageService   = NewError(ErrCodeStorageService, "")
	ErrExternalService  = NewError(ErrCodeExternalService, "")
	ErrTimeout          = NewError(ErrCodeTimeout, "")
)

Predefined errors for common scenarios

Functions

func BadRequest

func BadRequest(w http.ResponseWriter, r *http.Request, message string, opts ...Option)

BadRequest responds with 400 Bad Request

func BindError

func BindError(w http.ResponseWriter, r *http.Request, details any, opts ...Option)

BindError responds with 400 Bad Request for binding errors

func Conflict

func Conflict(w http.ResponseWriter, r *http.Request, message string, opts ...Option)

Conflict responds with 409 Conflict

func Created

func Created(w http.ResponseWriter, r *http.Request, data any, opts ...Option)

Created responds with 201 Created and data

func CustomError

func CustomError(w http.ResponseWriter, r *http.Request, status int, code int, message string, details any, opts ...Option)

CustomError responds with custom status code and error

func DatabaseError

func DatabaseError(w http.ResponseWriter, r *http.Request, message string, opts ...Option)

DatabaseError responds with 500 Internal Server Error for database errors

func DefaultPanicFn

func DefaultPanicFn(w http.ResponseWriter, r *http.Request, err error)

func Forbidden

func Forbidden(w http.ResponseWriter, r *http.Request, message string, opts ...Option)

Forbidden responds with 403 Forbidden

func GetErrorMessage

func GetErrorMessage(code int) string

GetErrorMessage returns the default message for an error code

func InternalServerError

func InternalServerError(w http.ResponseWriter, r *http.Request, message string, opts ...Option)

InternalServerError responds with 500 Internal Server Error

func NoContent

func NoContent(w http.ResponseWriter, r *http.Request, opts ...Option)

NoContent responds with 204 No Content

func NotFound

func NotFound(w http.ResponseWriter, r *http.Request, message string, opts ...Option)

NotFound responds with 404 Not Found

func OK

func OK(w http.ResponseWriter, r *http.Request, data any, opts ...Option)

OK responds with 200 OK and data

func ServiceUnavailable

func ServiceUnavailable(w http.ResponseWriter, r *http.Request, message string, opts ...Option)

ServiceUnavailable responds with 503 Service Unavailable

func TooManyRequests

func TooManyRequests(w http.ResponseWriter, r *http.Request, message string, opts ...Option)

TooManyRequests responds with 429 Too Many Requests

func Unauthorized

func Unauthorized(w http.ResponseWriter, r *http.Request, message string, opts ...Option)

Unauthorized responds with 401 Unauthorized

func ValidationError

func ValidationError(w http.ResponseWriter, r *http.Request, details any, opts ...Option)

ValidationError responds with 400 Bad Request and validation details Accepts any type of details ([]FieldError, map[string]string, or custom struct)

func Write

func Write(w http.ResponseWriter, r *http.Request, status int, data any, opts ...Option)

Write sends a success response with data

func WriteError

func WriteError(w http.ResponseWriter, r *http.Request, status int, err Error, opts ...Option)

WriteError sends an error response

func WriteList

func WriteList(w http.ResponseWriter, r *http.Request, status int, data any, pager *PaginationMeta, opts ...Option)

WriteList sends a success response with data and pagination

Types

type Error

type Error struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
	Details any    `json:"details,omitempty"`
}

Error represents the error structure in API responses

func NewError

func NewError(code int, message string) Error

NewError creates a new Error with code and message

func NewErrorWithDetails

func NewErrorWithDetails(code int, message string, details any) Error

NewErrorWithDetails creates a new Error with code, message and details

type ErrorConfig

type ErrorConfig struct {
	Code       int    // Error code (e.g., 4001, 5001)
	Message    string // Default error message
	HTTPStatus int    // HTTP status code (e.g., 400, 500)
}

ErrorConfig defines custom error code configuration

type FactoryOption

type FactoryOption func(*ResponderFactory)

FactoryOption defines configuration options for ResponderFactory

func WithCustomErrors

func WithCustomErrors(errors map[int]*ErrorConfig) FactoryOption

WithCustomErrors adds custom error code mappings Example:

WithCustomErrors(map[int]*ErrorConfig{
  4100: {Code: 4100, Message: "User not found", HTTPStatus: 404},
  5100: {Code: 5100, Message: "Payment service error", HTTPStatus: 500},
})

func WithPanicFn

func WithPanicFn(panicFn PanicFn) FactoryOption

WithPanicFn sets a custom panic handler

type FieldError

type FieldError struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

FieldError represents a validation error for a specific field

type Meta

type Meta struct {
	TraceId    string          `json:"traceId,omitempty"`
	Took       int64           `json:"took,omitempty"`
	Pagination *PaginationMeta `json:"pagination,omitempty"`
}

Meta represents metadata in API responses

func NewMeta

func NewMeta(opts ...Option) *Meta

type Option

type Option func(*Meta)

func WithPagination

func WithPagination(p *PaginationMeta) Option

func WithTook

func WithTook(ms int64) Option

func WithTraceID

func WithTraceID(id string) Option

type PaginationMeta

type PaginationMeta struct {
	Page       int   `json:"page"`
	PageSize   int   `json:"pageSize"`
	Total      int64 `json:"total"`
	TotalPages int   `json:"totalPages"`
	HasMore    bool  `json:"hasMore"`
}

PaginationMeta represents pagination information

type PanicFn

type PanicFn func(http.ResponseWriter, *http.Request, error)

type Responder

type Responder struct {
	// contains filtered or unexported fields
}

func New

func New(w http.ResponseWriter, r *http.Request, panicFn PanicFn) *Responder

func (*Responder) BadRequest

func (r *Responder) BadRequest(message string, opts ...Option)

BadRequest responds with 400 Bad Request

func (*Responder) BindError

func (r *Responder) BindError(details any, opts ...Option)

BindError responds with 400 Bad Request for binding errors

func (*Responder) Conflict

func (r *Responder) Conflict(message string, opts ...Option)

Conflict responds with 409 Conflict

func (*Responder) Created

func (r *Responder) Created(data any, opts ...Option)

Created responds with 201 Created and data

func (*Responder) CustomError

func (r *Responder) CustomError(status int, code int, message string, details any, opts ...Option)

CustomError responds with custom status code and error

func (*Responder) DatabaseError

func (r *Responder) DatabaseError(message string, opts ...Option)

DatabaseError responds with 500 Internal Server Error for database errors

func (*Responder) Forbidden

func (r *Responder) Forbidden(message string, opts ...Option)

Forbidden responds with 403 Forbidden

func (*Responder) GetCustomError

func (r *Responder) GetCustomError(code int) *ErrorConfig

GetCustomError retrieves custom error configuration by code

func (*Responder) InternalServerError

func (r *Responder) InternalServerError(message string, opts ...Option)

InternalServerError responds with 500 Internal Server Error

func (*Responder) NoContent

func (r *Responder) NoContent(opts ...Option)

NoContent responds with 204 No Content

func (*Responder) NotFound

func (r *Responder) NotFound(message string, opts ...Option)

NotFound responds with 404 Not Found

func (*Responder) OK

func (r *Responder) OK(data any, opts ...Option)

OK responds with 200 OK and data

func (*Responder) ServiceUnavailable

func (r *Responder) ServiceUnavailable(message string, opts ...Option)

ServiceUnavailable responds with 503 Service Unavailable

func (*Responder) TooManyRequests

func (r *Responder) TooManyRequests(message string, opts ...Option)

TooManyRequests responds with 429 Too Many Requests

func (*Responder) Unauthorized

func (r *Responder) Unauthorized(message string, opts ...Option)

Unauthorized responds with 401 Unauthorized

func (*Responder) ValidationError

func (r *Responder) ValidationError(details any, opts ...Option)

ValidationError responds with 400 Bad Request and validation details Accepts []FieldError for detailed field-level errors

func (*Responder) Write

func (r *Responder) Write(status int, payload any, opts ...Option)

Write sends a success response with data

func (*Responder) WriteError

func (r *Responder) WriteError(status int, err Error, opts ...Option)

WriteError sends an error response

func (*Responder) WriteList

func (r *Responder) WriteList(status int, payload any, pager *PaginationMeta, opts ...Option)

WriteList sends a success response with data and pagination

type ResponderFactory

type ResponderFactory struct {
	// contains filtered or unexported fields
}

func NewResponderFactory

func NewResponderFactory(opts ...FactoryOption) *ResponderFactory

NewResponderFactory creates a new ResponderFactory with options

func (*ResponderFactory) FromRequest

func (f *ResponderFactory) FromRequest(w http.ResponseWriter, r *http.Request) *Responder

type Response

type Response struct {
	Data  any    `json:"data,omitempty"`
	Error *Error `json:"error,omitempty"`
	Meta  Meta   `json:"meta"`
}

Response represents the standard API response structure

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL