api-design — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited api-design (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Design APIs that are consistent, predictable, and well-documented. This skill covers REST and GraphQL API design patterns.
Core principle: APIs are contracts. Once published, breaking changes are expensive.
Use this skill:
Not needed for:
✅ Good ❌ Bad
-----------------------------------------
GET /users GET /getUsers
GET /users/{id} GET /user?id=123
POST /users POST /createUser
PUT /users/{id} POST /updateUser
DELETE /users/{id} GET /deleteUser/{id}
GET /users/{id}/orders GET /getUserOrdersRules:
/users not /user/users/{id}/orders/user-profiles not /userProfiles| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Retrieve resource | Yes | Yes |
| POST | Create resource | No | No |
| PUT | Replace resource | Yes | No |
| PATCH | Partial update | Yes* | No |
| DELETE | Remove resource | Yes | No |
*PATCH is idempotent if using JSON Patch/Merge Patch format.
2xx Success
-----------------------------------------
200 OK - GET/PUT/PATCH success, body contains data
201 Created - POST success, Location header has URL
204 No Content - DELETE success, no body
4xx Client Error
-----------------------------------------
400 Bad Request - Invalid input (validation failed)
401 Unauthorized - Authentication required
403 Forbidden - Authenticated but not allowed
404 Not Found - Resource doesn't exist
409 Conflict - State conflict (duplicate, version mismatch)
422 Unprocessable - Valid syntax but semantic error
5xx Server Error
-----------------------------------------
500 Internal Error - Unexpected server error
502 Bad Gateway - Upstream service failed
503 Unavailable - Temporary overloadSuccess Response:
{
"data": {
"id": "123",
"name": "Example"
},
"meta": {
"requestId": "abc-123",
"timestamp": "2024-01-15T10:30:00Z"
}
}Collection Response:
{
"data": [
{ "id": "1", "name": "First" },
{ "id": "2", "name": "Second" }
],
"meta": {
"total": 100,
"page": 1,
"pageSize": 20,
"hasNext": true
}
}Error Response:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{ "field": "email", "message": "Invalid email format" }
]
},
"meta": {
"requestId": "abc-123",
"timestamp": "2024-01-15T10:30:00Z"
}
}| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URI Path | /v1/users | Clear, cacheable | URL changes |
| Header | Accept: application/vnd.api+json;version=1 | Clean URLs | Hidden |
| Query Param | /users?version=1 | Easy to test | Pollutes params |
Recommended: URI Path versioning for simplicity and clarity.
/v1/users # Version 1
/v2/users # Version 2 (breaking changes)When to increment version:
Non-breaking (no version bump):
openapi: 3.1.0
info:
title: User API
version: 1.0.0
description: User management endpoints
paths:
/users:
get:
summary: List users
operationId: listUsers
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: pageSize
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
post:
summary: Create user
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: Created
headers:
Location:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
'400':
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
components:
schemas:
User:
type: object
required:
- id
- email
properties:
id:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
createdAt:
type: string
format: date-time
ErrorResponse:
type: object
required:
- error
properties:
error:
type: object
required:
- code
- message
properties:
code:
type: string
message:
type: string
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: stringtype Query {
user(id: ID!): User
users(filter: UserFilter, pagination: Pagination): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!
}
type User {
id: ID!
email: String!
name: String
orders(first: Int, after: String): OrderConnection!
createdAt: DateTime!
}
# Relay-style connection for pagination
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
cursor: String!
node: User!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Input types
input CreateUserInput {
email: String!
name: String
}
# Payload types with union for errors
type CreateUserPayload {
user: User
errors: [UserError!]
}
type UserError {
field: String
message: String!
code: String!
}// ❌ Bad: N+1 queries
const resolvers = {
User: {
orders: async (user) => {
// Called once per user - N queries
return db.orders.findByUserId(user.id);
}
}
};
// ✅ Good: DataLoader batching
import DataLoader from 'dataloader';
const orderLoader = new DataLoader(async (userIds) => {
// Single batched query
const orders = await db.orders.findByUserIds(userIds);
return userIds.map(id => orders.filter(o => o.userId === id));
});
const resolvers = {
User: {
orders: async (user) => orderLoader.load(user.id)
}
};// Prevent expensive queries
const complexityConfig = {
maximumComplexity: 1000,
scalarCost: 1,
objectCost: 10,
listFactor: 10, // Multiply by list size
};
// Example: users(first: 100) { orders(first: 50) { ... } }
// Complexity: 10 + (100 * (10 + (50 * 10))) = 51,010 (rejected)HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704067200
# When exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 60| Strategy | Description | Use Case |
|---|---|---|
| Fixed Window | X requests per minute | Simple APIs |
| Sliding Window | Rolling window | More accurate |
| Token Bucket | Refill tokens over time | Burst-friendly |
| Leaky Bucket | Constant output rate | Smooth traffic |
Client → API Gateway → Microservices
↓
- Authentication
- Rate Limiting
- Request Routing
- Response Caching
- Request/Response Transform
- Circuit BreakerBefore shipping an API:
| Mistake | Fix |
|---|---|
| Verbs in URLs | Use nouns: /users not /getUsers |
| Inconsistent naming | Pick one: camelCase or snake_case |
| Missing pagination | Always paginate lists |
| No error codes | Use machine-readable codes |
| Breaking changes | Version the API |
| Missing rate limits | Protect against abuse |
| N+1 in GraphQL | Use DataLoader |
| No request IDs | Add for debugging |
REST
────────────────────────────────────
GET /resources List all
GET /resources/{id} Get one
POST /resources Create
PUT /resources/{id} Replace
PATCH /resources/{id} Update
DELETE /resources/{id} Remove
GraphQL
────────────────────────────────────
Query Read operations
Mutation Write operations
Subscription Real-time updates
Connections (Relay)
────────────────────────────────────
edges List of edge objects
node The actual object
cursor Pagination cursor
pageInfo hasNextPage, etc.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.