graphql · bobmatnyc — скилл для Claude Code: что делает и как установить
Справочный скилл по GraphQL для ИИ-агентов: помогает проектировать схемы SDL, писать резолверы и настраивать клиентские запросы.
npx skills add bobmatnyc/claude-mpm-skills --skill graphql -a claude-codeЧто делает graphql
Скилл даёт ассистенту практическую базу по GraphQL: правила Schema Definition Language (SDL), запуск резолверов на @apollo/server и клиентские вызовы в @apollo/client. С этими инструкциями модель точнее строит схемы без лишней выборки данных и связывает вложенные сущности.
Внутри разобраны операции query и mutation, строгая типизация полей и работа с интерфейсами. Для сервера приводятся примеры на TypeScript с запуском через startStandaloneServer на порту 4000. Код клиента опирается на связку с кэшем InMemoryCache.
Скилл помечен флагом user-invocable: false. Модель подгружает его контекст автоматически при генерации кода для GraphQL.
Какие задачи решает
- Спроектировать схему API на языке SDL со связями между типами
- Написать резолверы для выборки и изменения данных в Apollo Server
- Составить точные запросы на клиенте без избыточных полей
- Описать входные типы input для безопасных мутаций
Примеры запросов агенту
- Спроектируй схему GraphQL для блога с авторами, постами и комментариями
- Напиши резолверы на TypeScript для создания и обновления постов в Apollo Server
- Сформируй клиентский запрос Apollo Client с переменными для получения профиля пользователя
Как установить graphql
1Через Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill graphql -a claude-code2Вручную: положить файлы
~/.claude/skills/graphql/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/graphql/
1Через Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill graphql -a codex2Вручную: положить файлы
.agents/skills/graphql/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill graphql -a cursor2Вручную: положить файлы
.cursor/skills/graphql/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install bobmatnyc/claude-mpm-skills graphql2Вручную: положить файлы
.github/skills/graphql/SKILL.md1Через Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill graphql -a gemini-cli2Командой платформы
gemini skills install https://github.com/bobmatnyc/claude-mpm-skills --path toolchains/universal/data/graphqlВручную: положить файлы
.devin/skills/graphql/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill graphql -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Вопросы о graphql
Как установить graphql в Claude Code?
Выполните команду npx skills add bobmatnyc/claude-mpm-skills --skill graphql -a claude-code. Файл скилла сохранится по пути ~/.claude/skills/graphql/SKILL.md.
Зачем нужен этот скилл?
Он обучает ассистента спецификации GraphQL, помогая создавать SDL-схемы, настраивать резолверы и писать типизированный клиентский код.
Можно ли вызвать этот скилл вручную?
Нет, параметр user-invocable: false отключает прямой запуск через слэш-команду. Модель обращается к инструкциям сама, когда вы запрашиваете работу с GraphQL.
Исходный текст
SKILL.md · MIT
---
name: graphql
description: GraphQL query language and runtime for APIs enabling clients to request exactly the data they need with strongly-typed schemas and single endpoint architecture.
user-invocable: false
disable-model-invocation: true
progressive_disclosure:
entry_point:
- summary
- when_to_use
- quick_start
full_content:
- core_concepts
- schema_definition
- type_system
- queries_mutations
- resolvers_dataloaders
- subscriptions
- error_handling
- schema_patterns
- server_implementations
- client_integrations
- typescript_codegen
- authentication
- performance
- file_uploads
- testing
- production_patterns
- framework_integration
- comparison
- migration
- best_practices
tokens:
entry: 70
full: 5500
---
# GraphQL Skill
## Summary
GraphQL is a query language and runtime for APIs that enables clients to request exactly the data they need. It provides a strongly-typed schema, single endpoint architecture, and eliminates over-fetching/under-fetching problems common in REST APIs.
## When to Use
- Building flexible APIs for multiple client types (web, mobile, IoT)
- Complex data requirements with nested relationships
- Mobile-first applications needing bandwidth efficiency
- Reducing API versioning complexity
- Real-time data with subscriptions
- Microservices aggregation and federation
- Developer experience with strong typing and introspection
## Quick Start
### 1. Define Schema (SDL)
```graphql
# schema.graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
publishedAt: DateTime
}
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
}
type Mutation {
createPost(title: String!, content: String!, authorId: ID!): Post!
updatePost(id: ID!, title: String, content: String): Post!
deletePost(id: ID!): Boolean!
}
```
### 2. Write Resolvers (TypeScript + Apollo Server)
```typescript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { readFileSync } from 'fs';
// Load schema
const typeDefs = readFileSync('./schema.graphql', 'utf-8');
// Mock data
const users = [
{ id: '1', name: 'Alice', email: '[email protected]' },
{ id: '2', name: 'Bob', email: '[email protected]' },
];
const posts = [
{ id: '1', title: 'GraphQL Intro', content: 'Learning GraphQL...', authorId: '1' },
{ id: '2', title: 'Apollo Server', content: 'Building APIs...', authorId: '1' },
];
// Resolvers
const resolvers = {
Query: {
user: (_, { id }) => users.find(u => u.id === id),
users: () => users,
post: (_, { id }) => posts.find(p => p.id === id),
},
Mutation: {
createPost: (_, { title, content, authorId }) => {
const post = {
id: String(posts.length + 1),
title,
content,
authorId,
};
posts.push(post);
return post;
},
updatePost: (_, { id, title, content }) => {
const post = posts.find(p => p.id === id);
if (!post) throw new Error('Post not found');
if (title) post.title = title;
if (content) post.content = content;
return post;
},
deletePost: (_, { id }) => {
const index = posts.findIndex(p => p.id === id);
if (index === -1) return false;
posts.splice(index, 1);
return true;
},
},
User: {
posts: (user) => posts.filter(p => p.authorId === user.id),
},
Post: {
author: (post) => users.find(u => u.id === post.authorId),
},
};
// Create server
const server = new ApolloServer({ typeDefs, resolvers });
startStandaloneServer(server, {
listen: { port: 4000 },
}).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
```
### 3. Query Data (Client)
```typescript
// Using Apollo Client
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000',
cache: new InMemoryCache(),
});
// Query
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
publishedAt
}
}
}
`;
const { data } = await client.query({
query: GET_USER,
variables: { id: '1' },
});
// Mutation
const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!, $authorId: ID!) {
createPost(title: $title, content: $content, authorId: $authorId) {
id
title
content
}
}
`;
const { data: postData } = await client.mutate({
mutation: CREATE_POST,
variables: {
title: 'New Post',
content: 'Hello GraphQL!',
authorId: '1',
},
});
```
---
## Core Concepts
### GraphQL Fundamentals
- **Schema-First Design**: Define API contract with Schema Definition Language (SDL)
- **Type Safety**: Strongly-typed schema enforced at runtime and build-time
- **Single Endpoint**: All queries and mutations go through one URL (e.g., `/graphql`)
- **Client-Specified Queries**: Clients request exactly what they need
- **Hierarchical Data**: Queries mirror the shape of returned data
- **Introspection**: Schema is self-documenting and queryable
### Operations
```graphql
# Query - Read data (GET-like)
query GetUser {
user(id: "1") {
name
}
}
# Mutation - Modify data (POST/PUT/DELETE-like)
mutation CreateUser {
createUser(name: "Alice", email: "[email protected]") {
id
name
}
}
# Subscription - Real-time updates (WebSocket)
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}
```
### Fields and Arguments
```graphql
type Query {
# Field with arguments
user(id: ID!): User
users(limit: Int = 10, offset: Int = 0): [User!]!
# Search with multiple arguments
searchPosts(
query: String!
category: String
limit: Int = 20
): [Post!]!
}
```
---
## Schema Definition Language (SDL)
### Basic Type Definition
```graphql
type User {
id: ID! # Non-null ID scalar
name: String! # Non-null String
email: String!
age: Int # Nullable Int
isActive: Boolean!
posts: [Post!]! # Non-null list of non-null Posts
profile: Profile # Nullable object type
}
type Profile {
bio: String
avatarUrl: String
website: String
}
type Post {
id: ID!
title: String!
content: String!
author: User!
tags: [String!] # Non-null list, nullable elements
publishedAt: DateTime
}
```
### Input Types (for mutations)
```graphql
input CreateUserInput {
name: String!
email: String!
age: Int
}
input UpdateUserInput {
name: String
email: String
age: Int
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
```
### Interfaces
```graphql
interface Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
type User implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
name: String!
email: String!
}
type Post implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
title: String!
content: String!
}
type Query {
node(id: ID!): Node # Can return User or Post
}
```
### Unions
```graphql
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
# Client query with fragments
query Search {
search(query: "graphql") {
... on User {
name
email
}
... on Post {
title
content
}
... on Comment {
text
author { name }
}
}
}
```
### Enums
```graphql
enum Role {
ADMIN
MODERATOR
USER
GUEST
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type User {
id: ID!
name: String!
role: Role!
}
type Post {
id: ID!
title: String!
status: PostStatus!
}
```
---
## Type System
### Scalar Types
```graphql
# Built-in scalars
scalar Int # Signed 32-bit integer
scalar Float # Signed double-precision floating-point
scalar String # UTF-8 character sequence
scalar Boolean # true or false
scalar ID # Unique identifier (serialized as String)
# Custom scalars
scalar DateTime # ISO 8601 timestamp
scalar Email # Email address
scalar URL # Valid URL
scalar JSON # Arbitrary JSON
scalar Upload # File upload
```
### Custom Scalar Implementation
```typescript
// DateTime scalar (TypeScript)
import { GraphQLScalarType, Kind } from 'graphql';
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO 8601 DateTime',
// Serialize to client (output)
serialize(value: Date) {
return value.toISOString();
},
// Parse from client (input)
parseValue(value: string) {
return new Date(value);
},
// Parse from query literal
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
return new Date(ast.value);
}
return null;
},
});
// Add to resolvers
const resolvers = {
DateTime: DateTimeScalar,
// ... other resolvers
};
```
### Non-Null and Lists
```graphql
type User {
name: String! # Non-null String
email: String # Nullable String
tags: [String!]! # Non-null list of non-null Strings
friends: [User!] # Nullable list of non-null Users
posts: [Post]! # Non-null list of nullable Posts
comments: [Comment] # Nullable list of nullable Comments
}
```
---
## Queries and Mutations
### Query Variables
```typescript
// Define query with variables
const GET_USER = gql`
query GetUser($id: ID!, $includePosts: Boolean = false) {
user(id: $id) {
id
name
email
posts @include(if: $includePosts) {
id
title
}
}
}
`;
// Execute with variables
const { data } = await client.query({
query: GET_USER,
variables: {
id: '1',
includePosts: true,
},
});
```
### Aliases
```graphql
query {
# Fetch same field with different arguments
user1: user(id: "1") {
name
}
user2: user(id: "2") {
name
}
# Alias for clarity
currentUser: me {
id
name
}
}
```
### Fragments
```graphql
# Define reusable fragment
fragment UserFields on User {
id
name
email
createdAt
}
fragment PostSummary on Post {
id
title
publishedAt
author {
...UserFields
}
}
# Use fragments in query
query {
user(id: "1") {
...UserFields
posts {
...PostSummary
}
}
}
```
### Directives
```graphql
# Built-in directives
query GetUser($id: ID!, $withPosts: Boolean!, $skipEmail: Boolean!) {
user(id: $id) {
name
email @skip(if: $skipEmail)
posts @include(if: $withPosts) {
title
}
}
}
# Custom directive definition
directive @auth(requires: Role = USER) on FIELD_DEFINITION
type Query {
users: [User!]! @auth(requires: ADMIN)
me: User! @auth
}
```
### Mutations Best Practices
```graphql
# Single mutation with input type
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload!
deletePost(id: ID!): DeletePostPayload!
}
input CreatePostInput {
title: String!
content: String!
categoryId: ID!
tags: [String!]
}
# Payload pattern for mutations
type CreatePostPayload {
post: Post # Created resource
userErrors: [UserError!]! # Client errors
success: Boolean!
}
type UserError {
field: String! # Which field caused error
message: String! # Human-readable message
}
```
---
## Resolvers and DataLoaders
### Resolver Signature
```typescript
type Resolver<TParent, TArgs, TContext, TResult> = (
parent: TParent, // Parent object
args: TArgs, // Field arguments
context: TContext, // Shared context (auth, db, etc.)
info: GraphQLResolveInfo // Query metadata
) => TResult | Promise<TResult>;
```
### Basic Resolvers
```typescript
const resolvers = {
Query: {
user: async (_, { id }, { db }) => {
return db.users.findById(id);
},
users: async (_, { limit = 10, offset = 0 }, { db }) => {
return db.users.findMany({ limit, offset });
},
},
Mutation: {
createUser: async (_, { input }, { db, userId }) => {
if (!userId) {
throw new Error('Authentication required');
}
const user = await db.users.create(input);
return { user, userErrors: [], success: true };
},
},
User: {
// Field resolver - only called if client requests 'posts'
posts: async (user, _, { db }) => {
return db.posts.findByAuthorId(user.id);
},
// Computed field
fullName: (user) => {
return `${user.firstName} ${user.lastName}`;
},
},
};
```
### The N+1 Problem
```typescript
// ❌ BAD - N+1 queries
const resolvers = {
Query: {
users: () => db.users.findMany(), // 1 query
},
User: {
// Called for EACH user - N queries!
posts: (user) => db.posts.findByAuthorId(user.id),
},
};
// Querying 100 users = 1 + 100 = 101 database queries!
```
### DataLoader Solution
```typescript
import DataLoader from 'dataloader';
// Batch function - receives array of keys
async function batchLoadPosts(authorIds: string[]) {
const posts = await db.posts.findByAuthorIds(authorIds);
// Group by author ID
const postsByAuthor = authorIds.map(authorId =>
posts.filter(post => post.authorId === authorId)
);
return postsByAuthor;
}
// Create context with loaders
function createContext({ req }) {
return {
db,
userId: req.userId,
loaders: {
posts: new DataLoader(batchLoadPosts),
},
};
}
// ✅ GOOD - Batched queries
const resolvers = {
Query: {
users: () => db.users.findMany(), // 1 query
},
User: {
// Uses DataLoader - batches all requests into 1 query!
posts: (user, _, { loaders }) => {
return loaders.posts.load(user.id);
},
},
};
// Querying 100 users = 1 + 1 = 2 database queries!
```
### Advanced DataLoader Patterns
```typescript
// DataLoader with caching
const userLoader = new DataLoader(
async (ids) => {
const users = await db.users.findByIds(ids);
return ids.map(id => users.find(u => u.id === id));
},
{
cache: true, // Enable caching (default)
maxBatchSize: 100, // Limit batch size
batchScheduleFn: (cb) => setTimeout(cb, 10), // Debounce batching
}
);
// Cache manipulation
userLoader.clear(id); // Clear single key
userLoader.clearAll(); // Clear entire cache
userLoader.prime(id, user); // Prime cache with value
```
---
## Subscriptions
### WebSocket Setup (Apollo Server)
```typescript
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import express from 'express';
const app = express();
const httpServer = createServer(app);
const schema = makeExecutableSchema({ typeDefs, resolvers });
// WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
],
});
await server.start();
app.use('/graphql', express.json(), expressMiddleware(server));
httpServer.listen(4000);
```
### Subscription Schema
```graphql
type Subscription {
postCreated: Post!
postUpdated(id: ID!): Post!
messageAdded(channelId: ID!): Message!
userStatusChanged(userId: ID!): UserStatus!
}
type Message {
id: ID!
text: String!
author: User!
channelId: ID!
createdAt: DateTime!
}
enum UserStatus {
ONLINE
OFFLINE
AWAY
}
```
### Subscription Resolvers (PubSub)
```typescript
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers = {
Mutation: {
createPost: async (_, { input }, { db }) => {
const post = await db.posts.create(input);
// Publish to subscribers
pubsub.publish('POST_CREATED', { postCreated: post });
return { post, success: true, userErrors: [] };
},
sendMessage: async (_, { channelId, text }, { db, userId }) => {
const message = await db.messages.create({
channelId,
text,
authorId: userId,
});
pubsub.publish(`MESSAGE_${channelId}`, {
messageAdded: message,
});
return message;
},
},
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
},
postUpdated: {
subscribe: (_, { id }) => pubsub.asyncIterator([`POST_UPDATED_${id}`]),
},
messageAdded: {
subscribe: (_, { channelId }) => {
return pubsub.asyncIterator([`MESSAGE_${channelId}`]);
},
},
},
};
```
### Client Subscriptions (Apollo Client)
```typescript
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';
// HTTP link for queries and mutations
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
});
// WebSocket link for subscriptions
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:4000/graphql',
})
);
// Split based on operation type
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
// Use subscription
const MESSAGES_SUBSCRIPTION = gql`
subscription OnMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
author {
name
}
createdAt
}
}
`;
function ChatComponent({ channelId }) {
const { data, loading } = useSubscription(MESSAGES_SUBSCRIPTION, {
variables: { channelId },
});
if (loading) return <p>Loading...</p>;
return <div>New message: {data.messageAdded.text}</div>;
}
```
### Redis PubSub (Production)
```typescript
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
const options = {
host: 'localhost',
port: 6379,
retryStrategy: (times) => Math.min(times * 50, 2000),
};
const pubsub = new RedisPubSub({
publisher: new Redis(options),
subscriber: new Redis(options),
});
// Use same as in-memory PubSub
pubsub.publish('POST_CREATED', { postCreated: post });
pubsub.asyncIterator(['POST_CREATED']);
```
---
## Error Handling
### Error Types
```typescript
import { GraphQLError } from 'graphql';
// Custom error classes
class AuthenticationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
});
}
}
class ForbiddenError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'FORBIDDEN',
http: { status: 403 },
},
});
}
}
class ValidationError extends GraphQLError {
constructor(message: string, invalidFields: Record<string, string>) {
super(message, {
extensions: {
code: 'BAD_USER_INPUT',
invalidFields,
},
});
}
}
```
### Throwing Errors in Resolvers
```typescript
const resolvers = {
Query: {
user: async (_, { id }, { db, userId }) => {
if (!userId) {
throw new AuthenticationError('Must be logged in');
}
const user = await db.users.findById(id);
if (!user) {
throw new GraphQLError('User not found', {
extensions: { code: 'NOT_FOUND' },
});
}
return user;
},
},
Mutation: {
createPost: async (_, { input }, { db, userId }) => {
const errors: Record<string, string> = {};
if (!input.title || input.title.length < 3) {
errors.title = 'Title must be at least 3 characters';
}
if (!input.content) {
errors.content = 'Content is required';
}
if (Object.keys(errors).length > 0) {
throw new ValidationError('Invalid input', errors);
}
const post = await db.posts.create({ ...input, authorId: userId });
return { post, success: true, userErrors: [] };
},
},
};
```
### Error Response Format
```json
{
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"],
"extensions": {
"code": "NOT_FOUND"
}
}
],
"data": {
"user": null
}
}
```
### Field-Level Error Handling
```typescript
// Nullable fields allow partial results
type Query {
user(id: ID!): User # null on error
users: [User!]! # throws on error
post(id: ID!): Post # null on error
}
// Resolver can return null instead of throwing
const resolvers = {
Query: {
user: async (_, { id }, { db }) => {
try {
return await db.users.findById(id);
} catch (error) {
console.error('Failed to fetch user:', error);
return null; // Returns null instead of error
}
},
},
};
```
---
## Schema Design Patterns
### Relay Cursor Connections
```graphql
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PostEdge {
node: Post!
cursor: String!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type Query {
posts(
first: Int
after: String
last: Int
before: String
): PostConnection!
}
```
### Relay Connection Resolver
```typescript
import { fromGlobalId, toGlobalId } from 'graphql-relay';
function encodeCursor(id: string): string {
return Buffer.from(`cursor:${id}`).toString('base64');
}
function decodeCursor(cursor: string): string {
return Buffer.from(cursor, 'base64').toString('utf-8').replace('cursor:', '');
}
const resolvers = {
Query: {
posts: async (_, { first = 10, after }, { db }) => {
const startId = after ? decodeCursor(after) : null;
// Fetch first + 1 to determine hasNextPage
const posts = await db.posts.findMany({
where: startId ? { id: { gt: startId } } : {},
take: first + 1,
orderBy: { createdAt: 'desc' },
});
const hasNextPage = posts.length > first;
const nodes = hasNextPage ? posts.slice(0, -1) : posts;
const edges = nodes.map(node => ({
node,
cursor: encodeCursor(node.id),
}));
return {
edges,
pageInfo: {
hasNextPage,
hasPreviousPage: !!after,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount: await db.posts.count(),
};
},
},
};
```
### Offset Pagination (Simpler)
```graphql
type PostsResponse {
posts: [Post!]!
total: Int!
hasMore: Boolean!
}
type Query {
posts(limit: Int = 10, offset: Int = 0): PostsResponse!
}
```
### Global Object Identification (Relay)
```graphql
interface Node {
id: ID! # Global unique ID
}
type User implements Node {
id: ID!
name: String!
}
type Post implements Node {
id: ID!
title: String!
}
type Query {
node(id: ID!): Node
}
```
```typescript
const resolvers = {
Query: {
node: async (_, { id }, { db }) => {
const { type, id: rawId } = fromGlobalId(id);
switch (type) {
case 'User':
return db.users.findById(rawId);
case 'Post':
return db.posts.findById(rawId);
default:
return null;
}
},
},
User: {
id: (user) => toGlobalId('User', user.id),
},
Post: {
id: (post) => toGlobalId('Post', post.id),
},
};
```
---
## Server Implementations
### Apollo Server (TypeScript)
```typescript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
plugins: [
// Custom plugin
{
async requestDidStart() {
return {
async willSendResponse({ response }) {
console.log('Response:', response);
},
};
},
},
],
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => ({
token: req.headers.authorization,
db: database,
}),
});
```
### GraphQL Yoga (Modern Alternative)
```typescript
import { createYoga, createSchema } from 'graphql-yoga';
import { createServer } from 'node:http';
const yoga = createYoga({
schema: createSchema({
typeDefs,
resolvers,
}),
graphiql: true,
context: ({ request }) => ({
userId: request.headers.get('x-user-id'),
}),
});
const server = createServer(yoga);
server.listen(4000, () => {
console.log('Server on http://localhost:4000/graphql');
});
```
### Graphene (Python/Django)
```python
import graphene
from graphene_django import DjangoObjectType
from .models import User, Post
class UserType(DjangoObjectType):
class Meta:
model = User
fields = '__all__'
class PostType(DjangoObjectType):
class Meta:
model = Post
fields = '__all__'
class Query(graphene.ObjectType):
users = graphene.List(UserType)
user = graphene.Field(UserType, id=graphene.ID(required=True))
posts = graphene.List(PostType)
def resolve_users(self, info):
return User.objects.all()
def resolve_user(self, info, id):
return User.objects.get(pk=id)
def resolve_posts(self, info):
return Post.objects.all()
class CreateUser(graphene.Mutation):
class Arguments:
name = graphene.String(required=True)
email = graphene.String(required=True)
user = graphene.Field(UserType)
success = graphene.Boolean()
def mutate(self, info, name, email):
user = User.objects.create(name=name, email=email)
return CreateUser(user=user, success=True)
class Mutation(graphene.ObjectType):
create_user = CreateUser.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
```
### Strawberry (Python, Modern)
```python
import strawberry
from typing import List, Optional
from datetime import datetime
@strawberry.type
class User:
id: strawberry.ID
name: str
email: str
created_at: datetime
@strawberry.type
class Post:
id: strawberry.ID
title: str
content: str
author_id: strawberry.ID
@strawberry.input
class CreateUserInput:
name: str
email: str
@strawberry.type
class Query:
@strawberry.field
def users(self) -> List[User]:
return User.objects.all()
@strawberry.field
def user(self, id: strawberry.ID) -> Optional[User]:
return User.objects.get(pk=id)
@strawberry.type
class Mutation:
@strawberry.mutation
def create_user(self, input: CreateUserInput) -> User:
return User.objects.create(**input.__dict__)
schema = strawberry.Schema(query=Query, mutation=Mutation)
# FastAPI integration
from strawberry.fastapi import GraphQLRouter
app = FastAPI()
app.include_router(GraphQLRouter(schema), prefix="/graphql")
```
---
## Client Integrations
### Apollo Client (React)
```typescript
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache(),
});
function App() {
return (
<ApolloProvider client={client}>
<UserList />
</ApolloProvider>
);
}
function UserList() {
const { loading, error, data, refetch } = useQuery(GET_USERS);
const [createUser] = useMutation(CREATE_USER, {
refetchQueries: [{ query: GET_USERS }],
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
<button onClick={() => createUser({ variables: { name: 'New User' } })}>
Add User
</button>
</div>
);
}
```
### urql (Lightweight Alternative)
```typescript
import { createClient, Provider, useQuery, useMutation } from 'urql';
const client = createClient({
url: 'http://localhost:4000/graphql',
});
function App() {
return (
<Provider value={client}>
<UserList />
</Provider>
);
}
function UserList() {
const [result, reexecuteQuery] = useQuery({ query: GET_USERS });
const [, createUser] = useMutation(CREATE_USER);
const { data, fetching, error } = result;
if (fetching) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
```
### graphql-request (Minimal)
```typescript
import { GraphQLClient, gql } from 'graphql-request';
const client = new GraphQLClient('http://localhost:4000/graphql');
async function fetchUsers() {
const query = gql`
query {
users {
id
name
}
}
`;
const data = await client.request(query);
return data.users;
}
async function createUser(name: string) {
const mutation = gql`
mutation CreateUser($name: String!) {
createUser(name: $name) {
id
name
}
}
`;
const data = await c...
© bobmatnyc · MIT · bobmatnyc/claude-mpm-skills
Похожие скиллы
Все скиллы по теме «Бэкенд и API» →Сначала — graphql от других авторов.