graphql
>-
Works with
---
name: graphql
description: >-
license: MIT
---
# GraphQL API Design and Implementation Rules
## 1. Schema Design Principles
### SDL-first vs Code-first
- **SDL-first** (recommended for most projects): Define the schema in `.graphqls`
files, then implement resolvers to match
- Encourages contract-first design — schema is the API contract
- Better for team collaboration — frontend and backend agree on schema first
- Tools: Spring for GraphQL, Apollo Server
- **Code-first**: Generate the schema from code annotations or DSL
- Useful when schema closely mirrors domain model
- Risk of coupling schema to implementation details
- Tools: Netflix DGS (annotation-based), GraphQL Kotlin, TypeGraphQL
### Schema Organization
```text
src/main/resources/graphql/
schema.graphqls # Root schema (Query, Mutation, Subscription)
types/
user.graphqls # User-related types
order.graphqls # Order-related types
inputs/
user-input.graphqls # User input types
enums/
status.graphqls # Enum definitions
interfaces/
node.graphqls # Shared interfaces
```
### Naming Conventions
- **Types**: PascalCase (`User`, `OrderItem`)
- **Fields**: camelCase (`firstName`, `createdAt`)
- **Enums**: PascalCase type, SCREAMING_SNAKE_CASE values (`OrderStatus.IN_PROGRESS`)
- **Mutations**: verb + noun (`createUser`, `updateOrder`, `deleteComment`)
- **Queries**: noun for single (`user`), plural for list (`users`)
- **Input types**: suffix with `Input` (`CreateUserInput`, `UpdateOrderInput`)
- **Payload types**: suffix with `Payload` (`CreateUserPayload`)
---
## 2. Type System
### Object Types
```graphql
type User {
id: ID!
email: String!
name: String!
avatar: String
orders(first: Int, after: String): OrderConnection!
createdAt: DateTime!
}
```
- Use `ID!` for identifiers — never `Int!` or `String!`
- Mark required fields with `!` (non-null)
- Use nullable fields (`String`) for optional data
- Add arguments to fields for filtering/pagination
### Input Types
```graphql
input CreateUserInput {
email: String!
name: String!
avatar: String
}
input UpdateUserInput {
email: String
name: String
avatar: String
}
```
- Separate input types for create and update operations
- Create inputs: required fields are non-null
- Update inputs: all fields nullable (partial update)
- Never reuse output types as input types
### Interface Types
```graphql
interface Node {
id: ID!
}
interface Timestamped {
createdAt: DateTime!
updatedAt: DateTime!
}
type User implements Node & Timestamped {
id: ID!
email: String!
createdAt: DateTime!
updatedAt: DateTime!
}
```
- Use interfaces for shared field contracts
- `Node` interface is standard for Relay-compatible APIs
### Union Types
```graphql
union SearchResult = User | Order | Product
type Query {
search(term: String!): [SearchResult!]!
}
```
- Use unions when return types share no common fields
- Prefer interfaces when types share fields
- Always handle all union members in the client
### Enum Types
```graphql
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
```
- Use enums for fixed sets of values
- Use SCREAMING_SNAKE_CASE for values
- Add descriptions for non-obvious values
### Custom Scalar Types
```graphql
scalar DateTime
scalar URL
scalar JSON
scalar BigDecimal
```
- Define custom scalars for domain-specific types
- Always provide serialization/deserialization logic
- Prefer specific scalars over `String` for validation
---
## 3. Resolver Patterns and DataLoader
### Resolver Structure
```text
Query.user(id) → UserResolver
User.orders → OrderResolver (field resolver)
Order.items → OrderItemResolver (field resolver)
```
- Keep resolvers thin — delegate business logic to service layer
- One resolver class per type or domain area
- Field resolvers load data lazily — only when requested
> See [references/advanced-patterns.md](references/advanced-patterns.md) for N+1 problem explanation, DataLoader implementation example, and mutation error patterns.
### DataLoader Rules
- Always use DataLoader for field resolvers that load related entities
- DataLoader batches requests within a single request context
- DataLoader also provides per-request caching
- Monitor batch sizes — unexpectedly large batches may indicate issues
### Resolver Best Practices
- Never call repositories directly from resolvers — use a service layer
- Handle null values explicitly in resolvers
- Use `@Secured` or custom directives for field-level authorization
- Log resolver errors with correlation IDs for debugging
---
## 4. Pagination
### Cursor-based Pagination (Relay Connection Spec)
```graphql
type Query {
users(first: Int, after: String, last: Int, before: String): UserConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
```
### Pagination Guidelines
- Prefer cursor-based over offset-based pagination
- Cursor-based: stable under concurrent inserts/deletes
- Offset-based: simple but breaks with data changes
- Cursors should be opaque strings (base64-encoded IDs)
- Always include `totalCount` when feasible
- Set reasonable defaults and maximums for `first`/`last` (e.g., default 20,
max 100)
- For simple use cases, offset-based pagination is acceptable:
```graphql
type Query {
users(page: Int = 1, size: Int = 20): UserPage!
}
type UserPage {
content: [User!]!
totalElements: Int!
totalPages: Int!
hasNext: Boolean!
}
```
---
## 5. Error Handling
### GraphQL Error Response Structure
```json
{
"data": null,
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND",
"classification": "DataFetchingException"
}
}
]
}
```
### Error Handling Patterns
- Use `extensions.code` for machine-readable error codes
- Define a consistent set of error codes across the API:
- `NOT_FOUND`, `VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`,
`INTERNAL_ERROR`, `CONFLICT`, `RATE_LIMITED`
- Return partial data when possible — GraphQL supports partial responses
- Never expose internal details (stack traces, SQL) in error messages
- Use union return types for mutations to provide typed error responses (see [references/advanced-patterns.md](references/advanced-patterns.md) for union-based mutation error pattern examples)
---
## 6. Query Complexity and Depth Limiting
### Depth Limiting
- Set a maximum query depth (recommended: 7-10 for general APIs)
- Reject queries exceeding the depth limit before execution
```graphql
# This nested query could be malicious if unbounded:
{
user {
friends {
friends {
friends {
friends { ... } # Excessive depth
}
}
}
}
}
```
### Complexity Analysis
- Assign complexity scores to fields based on cost:
- Scalar fields: 0-1
- Object fields: 1
- List fields: multiply by expected/requested count
- Database-heavy fields: higher weight
- Set a maximum total complexity per query (e.g., 1000)
- Return the complexity cost in response extensions for transparency
> For DGS framework configuration example, see [references/advanced-patterns.md](references/advanced-patterns.md).
---
## 7. Authentication and Authorization
### Authentication
- Authenticate at the transport layer (HTTP headers, cookies)
- Pass the authenticated user context to resolvers via GraphQL context
- Do NOT put authentication logic inside individual resolvers
### Field-Level Authorization
```graphql
type User {
id: ID!
name: String! # Public
email: String! @auth(role: SELF) # Only the user themselves
salary: BigDecimal @auth(role: ADMIN) # Admin only
}
```
- Use custom directives or schema annotations for declarative authorization
- Implement authorization in a middleware/interceptor layer, not in
business logic
- Return `null` for unauthorized nullable fields; throw for non-null fields
- Log authorization failures for security monitoring
### Authorization Patterns
- **Directive-based**: `@auth(role: ADMIN)` on schema fields
- **Resolver-based**: Check permissions in the resolver or service layer
- **DataLoader-based**: Filter results in DataLoader based on context
---
## 8. Subscriptions (Real-time)
### Subscription Design
```graphql
type Subscription {
orderStatusChanged(orderId: ID!): OrderStatusEvent!
newMessage(channelId: ID!): Message!
}
type OrderStatusEvent {
order: Order!
previousStatus: OrderStatus!
newStatus: OrderStatus!
changedAt: DateTime!
}
```
### Implementation Guidelines
- Use WebSocket (graphql-ws protocol) for subscriptions
- Keep subscription payloads small — include only changed data
- Always require authentication for subscriptions
- Set connection timeouts and maximum subscription limits per client
- Use server-sent events (SSE) as an alternative for simpler use cases
- Consider using a message broker (Redis Pub/Sub, Kafka) for scalability
---
## 9. Federation (Apollo Federation)
### When to Use Federation
- Multiple teams own different parts of the graph
- Microservice architecture where each service owns its domain types
- Need to compose a unified schema from independent subgraphs
> For subgraph schema examples, see [references/advanced-patterns.md](references/advanced-patterns.md).
### Federation Guidelines
- Each entity has exactly one owning subgraph
- Use `@key` directive to define entity identity
- Use `@external`, `@requires`, `@provides` for cross-subgraph fields
- Keep the gateway/router stateless — all logic in subgraphs
- Monitor subgraph latency — gateway adds overhead
- Version subgraphs independently; use schema registry for compatibility
---
## 10. Security
> For the full security reference, see the OWASP GraphQL Cheat Sheet.
### Introspection
- **Disable introspection in production** — it exposes the full schema
- Enable only in development/staging environments
### Query Restrictions
- Set maximum query depth (see Section 6)
- Set maximum query complexity (see Section 6)
- Limit batch queries (array of operations in single request)
- Set request size limits at the HTTP layer
- Implement rate limiting per client/IP
### Input Validation
- Validate all input arguments at the schema level (non-null, enums)
- Add custom validation in resolvers for business rules
- Sanitize string inputs to prevent injection attacks
- Use custom scalars with built-in validation (e.g., `Email`, `URL`)
### Persisted Queries
- Use automatic persisted queries (APQ) or pre-registered queries in
production
- APQ: client sends a hash; server looks up the query
- Prevents arbitrary query execution in sensitive environments
- Reduces bandwidth by not sending full query strings
### Additional Security Measures
- Enable CORS with strict origin policies
- Use HTTPS for all GraphQL endpoints
- Log and monitor query patterns for anomaly detection
- Set timeouts for resolver execution to prevent resource exhaustion
---
## 11. GraphQL vs REST Selection Criteria
### Prefer GraphQL When
- Clients need flexible data fetching (mobile vs web different needs)
- Multiple related entities are fetched in a single request
- API serves diverse clients with varying data requirements
- Real-time features (subscriptions) are needed
- Rapid frontend iteration without backend changes
### Prefer REST When
- Simple CRUD operations on well-defined resources
- File upload/download is a primary use case
- Caching at the HTTP layer (CDN, browser) is critical
- Team has limited GraphQL experience
- API is primarily server-to-server communication
- OpenAPI/Swagger tooling ecosystem is a requirement
### Hybrid Approach
- Use GraphQL for client-facing APIs with complex data requirements
- Use REST for internal service-to-service communication
- Use REST for file handling, health checks, and webhook endpoints
- A single application can expose both GraphQL and REST endpoints
---
## 12. Framework Reference
For detailed framework-specific implementation patterns with Spring for
GraphQL and Netflix DGS, see
[references/spring-graphql-dgs.md](references/spring-graphql-dgs.md).
For advanced schema design patterns, naming conventions, and mutation
design guidelines, see
[references/schema-design-patterns.md](references/schema-design-patterns.md).More API Design skills
lark-event
larksuite/cli
Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume <EventKey>` (covers IM messages/reactions/chat changes, Approval status changes, Task updates, VC meeting started/joined/ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses.
lark-contact
larksuite/cli
飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 open_id 反查姓名 / 部门 / 邮箱 / 联系方式 / 个人状态 / 签名,以及按关键词搜索当前用户可见的机器人 / 智能体(agent)。当用户提到一个名字要下一步发消息 / 排日程,或拿到 open_id 想查具体信息时使用。不负责部门树遍历、按部门列员工、组织架构图,这类需求走原生 OpenAPI。
lark-openapi-explorer
larksuite/cli
飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未经 CLI 封装的原生 OpenAPI 接口。当用户的需求无法被现有 lark-* skill 或 lark-cli 已注册命令满足,需要查找并调用原生飞书 OpenAPI 时使用。

