算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
backend-api-response【设计与创意】Use this skill when designing API response contracts — Response<T>, exception handling, error codes, pagination envelopes, standardized payload contracts. This skill enforces: uniform ApiResponse<T> envelope, UPPER_SNAKE_CASE error codes, pagination for list endpoints, ISO 8601 UTC timestamps. Do NOT use for: endpoint routing design, database schema,…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Define and enforce a universal API response contract with standardized envelopes, error codes, and pagination. Every response must be predictable regardless of success or failure.
User request includes: api response, response envelope, response<T>, api error, exception handling, error code, api contract, pagination response, api standard.
A markdown document containing:
Produce the artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions. Strip articles a/an/the where unambiguous. Compress output. If response format already standardized, output API response standard already defined at: [path]. and stop.
4096 tokens
What is the primary client type?
├── Browser SPA / Mobile app
│ ├── Need minimal payload overhead?
│ │ ├── Yes → Wrapper envelope (success+data+error+meta)
│ │ └── No → Standard envelope with full metadata
│ └── Need GraphQL-style flexibility?
│ ├── Yes → Single endpoint with error array pattern
│ └── No → RESTful per-endpoint envelope
├── Service-to-service (internal)
│ ├── Use OpenAPI-generated clients?
│ │ ├── Yes → Full envelope with requestId for tracing
│ │ └── No → Result type pattern (monads/railway)
└── Third-party / Public API
├── Use standard envelope with domain-prefixed error codes
└── Include rate limit headers in every response
Will the list have new items inserted while users browse?
├── Yes → Cursor-based pagination (stable, no duplicates/skips)
│ └── Encode cursor as base64 opaque string, not raw DB value
├── No → Do users need random page access?
│ ├── Yes → Offset-based pagination (page N access)
│ │ └── Warn: performance degrades with large offset
│ └── No → Keyset pagination (best performance, O(1))
└── Is this a real-time feed?
└── Cursor-based with live updates via WebSocket/SSE
Who is the consumer?
├── External developer (public API)
│ ├── Generic messages for 5xx, specific for 4xx
│ ├── Include details array for field-level validation
│ └── Never expose stack traces or internal IDs
├── Internal service
│ ├── Include error codes for automated handling
│ ├── Optionally include correlation ID for debugging
│ └── Structured machine-parseable error codes
└── End-user facing (BFF)
├── User-friendly messages (not error codes)
├── Include actionable remediation hints
└── Log full details server-side only
Standard ApiResponse<T> — Every API response MUST follow this envelope:
{
"success": true,
"data": { ... },
"error": null,
"metadata": {
"requestId": "req_abc123",
"timestamp": "2026-05-14T10:30:00Z",
"version": "1.0"
}
}
Error response:
{
"success": false,
"data": null,
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order with ID '123' not found",
"details": [
{ "field": "orderId", "reason": "Resource does not exist" }
],
"traceId": "trace_xyz789"
},
"metadata": {
"requestId": "req_abc123",
"timestamp": "2026-05-14T10:30:00Z",
"version": "1.0"
}
}
TypeScript
interface ApiResponse<T> {
success: boolean;
data: T | null;
error: ApiError | null;
metadata: ResponseMetadata;
}
interface ApiError {
code: string;
message: string;
details?: ErrorDetail[];
traceId: string;
}
interface ErrorDetail {
field: string;
reason: string;
}
interface ResponseMetadata {
requestId: string;
timestamp: string;
version: string;
}
C#
public class ApiResponse<T>
{
public bool Success { get; init; }
public T? Data { get; init; }
public ApiError? Error { get; init; }
public ResponseMetadata Metadata { get; init; } = new();
public static ApiResponse<T> Ok(T data) => new() {
Success = true, Data = data, Metadata = new()
};
public static ApiResponse<T> Fail(string code, string message, string traceId) => new() {
Success = false,
Error = new ApiError { Code = code, Message = message, TraceId = traceId },
Metadata = new()
};
}
Go
type ApiResponse[T any] struct {
Success bool `json:"success"`
Data *T `json:"data"`
Error *ApiError `json:"error"`
Metadata ResponseMetadata `json:"metadata"`
}
Python
from pydantic import BaseModel
from typing import Generic, TypeVar, Optional
T = TypeVar("T")
class ApiError(BaseModel):
code: str
message: str
details: Optional[list[dict]] = None
traceId: str
class ResponseMetadata(BaseModel):
requestId: str
timestamp: str
version: str = "1.0"
class ApiResponse(BaseModel, Generic[T]):
success: bool
data: Optional[T] = None
error: Optional[ApiError] = None
metadata: ResponseMetadata
@classmethod
def ok(cls, data: T, request_id: str) -> "ApiResponse[T]":
return cls(
success=True,
data=data,
metadata=ResponseMetadata(
requestId=request_id,
timestamp=datetime.utcnow().isoformat() + "Z"
)
)
@classmethod
def fail(cls, code: str, message: str, trace_id: str) -> "ApiResponse":
return cls(
success=False,
error=ApiError(code=code, message=message, traceId=trace_id),
metadata=ResponseMetadata(
requestId=trace_id,
timestamp=datetime.utcnow().isoformat() + "Z"
)
)
Java (Spring)
public record ApiResponse<T>(
boolean success,
T data,
ApiError error,
ResponseMetadata metadata
) {
public static <T> ApiResponse<T> ok(T data, String requestId) {
return new ApiResponse<>(true, data, null,
new ResponseMetadata(requestId, Instant.now().toString(), "1.0"));
}
public static <T> ApiResponse<T> fail(String code, String message, String traceId) {
return new ApiResponse<>(false, null,
new ApiError(code, message, null, traceId),
new ResponseMetadata(traceId, Instant.now().toString(), "1.0"));
}
}
Offset-based:
{
"success": true,
"data": [ ... ],
"pagination": {
"page": 1,
"pageSize": 20,
"totalCount": 150,
"totalPages": 8,
"hasNextPage": true,
"hasPreviousPage": false
},
"metadata": { ... }
}
Cursor-based:
{
"success": true,
"data": [ ... ],
"pagination": {
"cursor": "eyJpZCI6ICJhYmMxMjMifQ==",
"nextCursor": "eyJpZCI6ICJkZWY0NTYifQ==",
"prevCursor": null,
"hasMore": true
},
"metadata": { ... }
}
| Operation | Success | Validation Error | Not Found | Conflict | Server Error | |---|---|---|---|---|---|---| | GET (single) | 200 | — | 404 | — | 500 | | GET (list) | 200 | — | 200 (empty array) | — | 500 | | POST (create) | 201 | 422 | 404 (parent ref) | 409 | 500 | | PUT (update) | 200 | 422 | 404 | 409 | 500 | | PATCH (partial) | 200 | 422 | 404 | 409 | 500 | | DELETE | 204 | — | 404 | 409 | 500 | | ASYNC (accepted) | 202 | 422 | — | — | 500 |
| Exception | Status | Error Code |
|---|---|---|
ValidationException | 422 | VALIDATION_ERROR |
NotFoundException | 404 | NOT_FOUND |
ConflictException | 409 | CONFLICT |
UnauthorizedException | 401 | UNAUTHORIZED |
ForbiddenException | 403 | FORBIDDEN |
RateLimitException | 429 | RATE_LIMITED |
InternalException | 500 | INTERNAL_ERROR |
TimeoutException | 504 | GATEWAY_TIMEOUT |
BusinessRuleException | 400 | BUSINESS_RULE_VIOLATION |
ServiceUnavailableException | 503 | SERVICE_UNAVAILABLE |
{DOMAIN}_{ERROR} in UPPER_SNAKE_CASE:
ORDER_NOT_FOUND — specific resourcePAYMENT_DECLINED — business logic failureVALIDATION_ERROR — general validationINSUFFICIENT_FUNDS — business validationDUPLICATE_ENTRY — uniqueness violationAUTH_TOKEN_EXPIRED — token lifecycleRATE_LIMIT_EXCEEDED — quota exhaustionNode.js/Express middleware
import { Request, Response, NextFunction } from 'express';
import { v4 as uuid } from 'uuid';
class ExceptionHandler {
handle(err: Error, req: Request, res: Response, _next: NextFunction): void {
const requestId = req.headers['x-request-id'] as string || uuid();
const traceId = uuid();
if (err instanceof ValidationException) {
res.status(422).json(ApiResponse.fail('VALIDATION_ERROR', err.message, traceId));
} else if (err instanceof NotFoundException) {
res.status(404).json(ApiResponse.fail('NOT_FOUND', err.message, traceId));
} else if (err instanceof ConflictException) {
res.status(409).json(ApiResponse.fail('CONFLICT', err.message, traceId));
} else if (err instanceof UnauthorizedException) {
res.status(401).json(ApiResponse.fail('UNAUTHORIZED', err.message, traceId));
} else if (err instanceof ForbiddenException) {
res.status(403).json(ApiResponse.fail('FORBIDDEN', err.message, traceId));
} else {
console.error('Unhandled error:', err);
res.status(500).json(ApiResponse.fail('INTERNAL_ERROR', 'An unexpected error occurred', traceId));
}
}
}
.NET global exception filter/middleware
public class GlobalExceptionHandler : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try { await next(context); }
catch (NotFoundException ex) { await WriteResponse(context, 404, "NOT_FOUND", ex); }
catch (ValidationException ex) { await WriteResponse(context, 422, "VALIDATION_ERROR", ex); }
catch (ConflictException ex) { await WriteResponse(context, 409, "CONFLICT", ex); }
catch (Exception ex) { await WriteResponse(context, 500, "INTERNAL_ERROR", ex); }
}
private static async Task WriteResponse(HttpContext ctx, int status, string code, Exception ex)
{
ctx.Response.StatusCode = status;
ctx.Response.ContentType = "application/json";
var response = ApiResponse<object>.Fail(code, ex.Message, Activity.Current?.Id ?? ctx.TraceIdentifier);
await ctx.Response.WriteAsJsonAsync(response);
}
}
FastAPI exception handler
from fastapi import Request, JSONResponse
from fastapi.exceptions import RequestValidationError
@app.exception_handler(NotFoundException)
async def not_found_handler(request: Request, exc: NotFoundException):
return JSONResponse(
status_code=404,
content=ApiResponse.fail("NOT_FOUND", str(exc), request.headers.get("x-request-id", "")).model_dump()
)
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
details = [
{"field": ".".join(err["loc"]), "reason": err["type"], "message": err["msg"]}
for err in exc.errors()
]
return JSONResponse(
status_code=422,
content={
"success": False,
"data": None,
"error": {"code": "VALIDATION_ERROR", "message": "Validation failed", "details": details},
"metadata": {"requestId": request.headers.get("x-request-id", ""), "timestamp": datetime.utcnow().isoformat() + "Z"}
}
)
class ResponseBuilder<T> {
private statusCode = 200;
private headers: Record<string, string> = {};
private pagination?: PaginationMeta;
private rateLimit?: RateLimitInfo;
constructor(
private data: T | null,
private requestId: string
) {}
withStatus(code: number): this {
this.statusCode = code;
return this;
}
withHeader(name: string, value: string): this {
this.headers[name] = value;
return this;
}
withPagination(meta: PaginationMeta): this {
this.pagination = meta;
return this;
}
withRateLimit(info: RateLimitInfo): this {
this.rateLimit = info;
return this;
}
build(): { statusCode: number; body: ApiResponse<T>; headers: Record<string, string> } {
const body: ApiResponse<T> = {
success: true,
data: this.data,
error: null,
metadata: {
requestId: this.requestId,
timestamp: new Date().toISOString(),
version: '1.0',
},
};
if (this.pagination) {
(body as any).pagination = this.pagination;
}
const headers = { ...this.headers };
if (this.rateLimit) {
headers['X-RateLimit-Limit'] = String(this.rateLimit.limit);
headers['X-RateLimit-Remaining'] = String(this.rateLimit.remaining);
headers['X-RateLimit-Reset'] = String(this.rateLimit.reset);
}
return { statusCode: this.statusCode, body, headers };
}
}
data field is null when error is present, and vice versa{ data: [...] }requestId in every response for debugging correlation?fields=id,name,email)Bad: Some endpoints return { data: ... }, others return raw array
Problem: Clients must handle multiple response shapes
Fix: Always use the standard envelope
Bad: { "success": false, "error": "Not found" } with HTTP 200
Problem: Clients cannot use HTTP status-based error handling. Proxies treat it as success.
Fix: Always use appropriate HTTP status codes
Bad: Returning { "error": { "stack": "Error at line 42..." } }
Problem: Information disclosure. Attackers learn about your infrastructure.
Fix: Log stack traces server-side, return generic messages
Bad: GET /users returns all 1M users without pagination
Problem: Server memory exhaustion, network timeouts
Fix: Always paginate. Default limit=20, max limit=100.
Bad: Mix of NOT_FOUND, NotFound, not_found, 404
Problem: Clients cannot reliably parse error codes programmatically
Fix: Always UPPER_SNAKE_CASE: RESOURCE_NOT_FOUND
Bad: { "details": [{ "field": "password", "reason": "Wrong password for user john@example.com" }] }
Problem: Exposes user identity and sensitive field names
Fix: Mask sensitive fields, never reveal user existence
Bad: { "data": [...], "pagination": { "page": 1, "limit": 20 } }
Problem: Clients cannot render total pages or progress indicators
Fix: Always include totalCount (or hasMore for cursor-based)
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Cache-Control: no-store (for authenticated responses)
Content-Security-Policy: default-src 'none'
Every response should include rate limit information:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1700000000
Retry-After: 30 (on 429 responses only)
| Aspect | Envelope (wrapped) | No Envelope (raw) |
|---|---|---|
| Predictability | Always predictable shape | Varies |
| Error handling | Structured | Ad-hoc |
| Client code | Uniform parsing | Per-endpoint logic |
| Over-fetching | Metadata overhead | Minimal |
| Cache efficiency | Lower (different keys) | Higher |
| Streaming | Harder | Easier |
| Aspect | Offset | Cursor | Keyset |
|---|---|---|---|
| Stability | Unstable with inserts | Stable | Stable |
| Performance | Degrades with large offset | O(1) with index | O(1) with index |
| Random access | Supported | Not supported | Not supported |
| Implementation | Simple | Moderate | Moderate |
| Real-time feeds | Poor | Excellent | Good |
| Total count | Included | Not available | Not available |
| Aspect | RFC 7807 | Custom Envelope |
|---|---|---|
| Standardization | RFC standard | Proprietary |
| Extensibility | Via extension members | Fully controlled |
| Tooling support | Growing | Custom |
| Simplicity | Moderate | Simple |
| Machine readability | Standard fields | Consistent shape |
omitempty/JsonIgnore(Condition.WhenWritingNull) to skip null fieldsCache-Control headers for GET responsesETag and Last-Modified for conditional requestsApiResponse<T> envelope. No exceptions.Hand off to backend/universal/api-design/SKILL.md for endpoint routing and resource design. Hand off to backend/universal/oop-principles/SKILL.md for exception class hierarchy design.