smithery/survikrowa

graphql-integration

GraphQL code generation and integration patterns across API, web, and native apps. Use this skill when setting up GraphQL operations, configuring code generation, or integrating GraphQL queries and mutations.

Installation

$ npx skills add smithery/survikrowa --skill graphql-integration

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/survikrowa.

npx skills add smithery/survikrowa

Browse all from smithery/survikrowa

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

LicenseMIT

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 18,644 B
  • docs SUMMARY.md 235 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

GraphQL Integration Skill

Overview

This skill covers GraphQL schema, code generation, and integration patterns used across all three applications in the Game Critique project.

GraphQL Architecture

Code-First vs Schema-First

  • API (NestJS): Code-first approach - schema auto-generated from TypeScript decorators
  • Web & Native: Schema-first - consume generated schema from API

Schema Location

# graphql.config.yml (root)
schema: "apps/api/src/schema.gql"

The schema is automatically generated by NestJS on startup and stored at apps/api/src/schema.gql.

API: Code-First Schema Generation

Creating GraphQL Types

// DTOs serve as GraphQL types
import { ObjectType, Field, Int, ArgsType, InputType } from '@nestjs/graphql';

@ObjectType()
export class GameDTO {
  @Field(() => Int)
  hltbId: number;

  @Field()
  title: string;

  @Field({ nullable: true })
  description?: string;

  @Field(() => [String])
  platforms: string[];

  @Field(() => Float, { nullable: true })
  rating?: number;
}

@ObjectType()
export class PaginatedGamesDTO {
  @Field(() => [GameDTO])
  games: GameDTO[];

  @Field(() => Int)
  total: number;

  @Field(() => Int)
  page: number;
}

Input Types and Arguments

@InputType()
export class CreateGameInput {
  @Field()
  title: string;

  @Field({ nullable: true })
  description?: string;

  @Field(() => [Int])
  platformIds: number[];
}

@ArgsType()
export class GetGamesArgs {
  @Field({ nullable: true })
  search?: string;

  @Field(() => Int, { defaultValue: 10 })
  take: number;

  @Field(() => Int, { defaultValue: 0 })
  skip: number;
}

Resolvers with Queries and Mutations

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';

@Resolver(() => GameDTO)
export class GamesResolver {
  constructor(private readonly gamesService: GamesService) {}

  @Query(() => GameDTO, { name: 'game', nullable: true })
  async getGame(@Args('id', { type: () => Int }) id: number) {
    return this.gamesService.findById(id);
  }

  @Query(() => PaginatedGamesDTO, { name: 'games' })
  async getGames(@Args() args: GetGamesArgs) {
    return this.gamesService.findAll(args);
  }

  @Mutation(() => GameDTO, { name: 'createGame' })
  async createGame(@Args('input') input: CreateGameInput) {
    return this.gamesService.create(input);
  }

  @Mutation(() => Boolean, { name: 'deleteGame' })
  async deleteGame(@Args('id', { type: () => Int }) id: number) {
    await this.gamesService.delete(id);
    return true;
  }
}

Custom Scalars

import { Scalar, CustomScalar } from '@nestjs/graphql';
import { Kind, ValueNode } from 'graphql';

@Scalar('DateTime')
export class DateTimeScalar implements CustomScalar<string, Date> {
  description = 'DateTime custom scalar type';

  parseValue(value: string): Date {
    return new Date(value);
  }

  serialize(value: Date): string {
    return value.toISOString();
  }

  parseLiteral(ast: ValueNode): Date {
    if (ast.kind === Kind.STRING) {
      return new Date(ast.value);
    }
    return null;
  }
}

Field Resolvers

@Resolver(() => UserDTO)
export class UserResolver {
  @ResolveField(() => [GameDTO])
  async games(@Parent() user: UserDTO) {
    return this.gamesService.findByUserId(user.id);
  }

  @ResolveField(() => Int)
  async gameCount(@Parent() user: UserDTO) {
    return this.gamesService.countByUserId(user.id);
  }
}

Native App: Apollo Client Integration

Code Generation Configuration

// apps/native/codegen.ts
import { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  schema: process.env.EXPO_PUBLIC_GRAPHQL_ENDPOINT || "http://localhost:3000/graphql",
  documents: ["modules/**/*.graphql"],
  generates: {
    "__generated__/types.ts": {
      plugins: ["typescript"]
    },
    "./": {
      preset: "near-operation-file",
      presetConfig: {
        extension: ".generated.ts",
        baseTypesPath: "__generated__/types.ts",
      },
      plugins: ["typescript-operations", "typescript-react-apollo"],
      config: {
        withHooks: true,
      },
    },
  },
};

export default config;

Writing GraphQL Operations

# modules/games_status/use_game_status/game_status.graphql
query GetGameStatus($gameId: Int!) {
  gameStatus(gameId: $gameId) {
    id
    status
    rating
    hoursPlayed
    completedAt
    game {
      hltbId
      title
      coverUrl
    }
  }
}

mutation UpsertGameStatus($input: UpsertGameStatusArgsDTO!) {
  upsertGameStatus(upsertGameStatusArgs: $input) {
    message
  }
}

Using Generated Apollo Hooks

import {
  useGetGameStatusQuery,
  useUpsertGameStatusMutation,
} from './game_status.generated';

export function GameStatusScreen({ gameId }) {
  // Query with loading and error states
  const { data, loading, error, refetch } = useGetGameStatusQuery({
    variables: { gameId },
    fetchPolicy: 'cache-and-network',
  });

  // Mutation with optimistic updates
  const [upsertStatus, { loading: saving }] = useUpsertGameStatusMutation({
    onCompleted: () => {
      toast.show('Status updated!');
      refetch();
    },
    onError: (error) => {
      toast.show(`Error: ${error.message}`);
    },
  });

  const handleSubmit = (values) => {
    upsertStatus({
      variables: {
        input: {
          gameId,
          status: values.status,
          rating: values.rating,
          hoursPlayed: values.hoursPlayed,
        },
      },
      optimisticResponse: {
        upsertGameStatus: {
          __typename: 'UpsertGameStatusResponse',
          message: 'Updating...',
        },
      },
    });
  };

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <GameStatusForm
      initialData={data?.gameStatus}
      onSubmit={handleSubmit}
      isSubmitting={saving}
    />
  );
}

Apollo Client Setup

// modules/graphql/apollo_provider.tsx
import { ApolloClient, InMemoryCache, createHttpLink, ApolloProvider } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import * as SecureStore from 'expo-secure-store';

const httpLink = createHttpLink({
  uri: process.env.EXPO_PUBLIC_GRAPHQL_ENDPOINT,
});

const authLink = setContext(async (_, { headers }) => {
  const token = await SecureStore.getItemAsync('authToken');
  
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    },
  };
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          games: {
            keyArgs: ['search'],
            merge(existing, incoming, { args }) {
              if (args?.skip === 0) return incoming;
              return [...(existing || []), ...incoming];
            },
          },
        },
      },
    },
  }),
});

export function ApolloProvider({ children }) {
  return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

Cache Management

// Manually update cache after mutation
const [deleteGame] = useDeleteGameMutation({
  update(cache, { data }) {
    cache.modify({
      fields: {
        games(existingGames, { readField }) {
          return existingGames.filter(
            (gameRef) => readField('id', gameRef) !== data.deleteGame.id
          );
        },
      },
    });
  },
});

// Refetch queries
const [createGame] = useCreateGameMutation({
  refetchQueries: ['GetGames', 'GetUserStats'],
  awaitRefetchQueries: true,
});

// Invalidate cache
const client = useApolloClient();
await client.resetStore(); // Clear entire cache
await client.refetchQueries({ include: ['GetGames'] }); // Refetch specific queries

Web App: React Query Integration

Code Generation Configuration

// apps/web/codegen-dev.ts
import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  schema: "http://localhost:3001/graphql",
  documents: "src/**/*.graphql",
  ignoreNoDocuments: true,
  generates: {
    "src/types.ts": {
      plugins: ["typescript"],
    },
    "src/": {
      preset: "near-operation-file",
      presetConfig: {
        extension: ".generated.ts",
        baseTypesPath: "types.ts",
      },
      plugins: ["typescript-operations", "typescript-react-query"],
      config: {
        withHooks: true,
        fetcher: "@/codegen/fetcher#fetchData",
        reactQueryVersion: 5,
      },
    },
  },
};

export default config;

Custom Fetcher

// src/codegen/fetcher.ts
export async function fetchData<TData, TVariables>(
  query: string,
  variables?: TVariables,
  options?: RequestInit['headers']
): Promise<TData> {
  const response = await fetch('http://localhost:3001/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...options,
    },
    body: JSON.stringify({
      query,
      variables,
    }),
  });

  const json = await response.json();

  if (json.errors) {
    const { message } = json.errors[0];
    throw new Error(message);
  }

  return json.data;
}

Fetcher with Auth

// Enhanced fetcher with Auth0 token
import { useAuth0 } from '@auth0/auth0-react';

export function useGraphQLFetcher() {
  const { getAccessTokenSilently } = useAuth0();

  return async <TData, TVariables>(
    query: string,
    variables?: TVariables
  ): Promise<TData> => {
    const token = await getAccessTokenSilently();
    
    return fetchData(query, variables, {
      authorization: `Bearer ${token}`,
    });
  };
}

Writing GraphQL Operations

# features/admin/users/use_users/users.graphql
query GetUsers($search: String, $take: Int!, $skip: Int!) {
  users(search: $search, take: $take, skip: $skip) {
    users {
      id
      oauthId
      profile {
        name
        email
        avatarUrl
      }
      role {
        role
      }
    }
    total
  }
}

mutation DeleteUser($userId: Int!) {
  deleteUser(userId: $userId)
}

Using Generated React Query Hooks

import { useGetUsersQuery, useDeleteUserMutation } from './users.generated';
import { useQueryClient } from '@tanstack/react-query';

export function UsersTable() {
  const [page, setPage] = useState(1);
  const [search, setSearch] = useState('');

  // Query with pagination
  const { data, isLoading, error } = useGetUsersQuery({
    search,
    take: 10,
    skip: (page - 1) * 10,
  }, {
    staleTime: 5 * 60 * 1000, // 5 minutes
    keepPreviousData: true,
  });

  // Mutation with cache invalidation
  const queryClient = useQueryClient();
  const { mutate: deleteUser, isPending } = useDeleteUserMutation({
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['GetUsers'] });
      toast.success('User deleted');
    },
    onError: (error) => {
      toast.error(error.message);
    },
  });

  if (isLoading) return <TableSkeleton />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <div>
      <input
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="Search users..."
      />
      
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Email</th>
            <th>Role</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {data?.users.users.map((user) => (
            <tr key={user.id}>
              <td>{user.profile?.name}</td>
              <td>{user.profile?.email}</td>
              <td>{user.role?.role}</td>
              <td>
                <button
                  onClick={() => deleteUser({ userId: user.id })}
                  disabled={isPending}
                >
                  Delete
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>

      <Pagination
        currentPage={page}
        totalPages={Math.ceil(data.users.total / 10)}
        onPageChange={setPage}
      />
    </div>
  );
}

Optimistic Updates

const { mutate } = useUpdateUserRoleMutation({
  onMutate: async (variables) => {
    // Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['GetUsers'] });

    // Snapshot previous value
    const previousUsers = queryClient.getQueryData(['GetUsers']);

    // Optimistically update cache
    queryClient.setQueryData(['GetUsers'], (old) => {
      return {
        ...old,
        users: old.users.map((user) =>
          user.id === variables.userId
            ? { ...user, role: { role: variables.role } }
            : user
        ),
      };
    });

    return { previousUsers };
  },
  onError: (err, variables, context) => {
    // Rollback on error
    queryClient.setQueryData(['GetUsers'], context.previousUsers);
  },
  onSettled: () => {
    // Always refetch after error or success
    queryClient.invalidateQueries({ queryKey: ['GetUsers'] });
  },
});

GraphQL Code Generation Workflow

Step-by-Step Process

  1. Define operation in API
// apps/api/src/modules/games/games.resolver.ts
@Query(() => GameDTO, { name: 'game' })
async getGame(@Args('id', { type: () => Int }) id: number) {
  return this.gamesService.findById(id);
}
  1. Start API to generate schema
cd apps/api
yarn dev  # Schema auto-generated at src/schema.gql
  1. Create GraphQL operation file
# apps/native/modules/games/use_game/game.graphql
query GetGame($id: Int!) {
  game(id: $id) {
    hltbId
    title
    description
    platforms
    rating
  }
}
  1. Run code generation
# Native app
cd apps/native
yarn generate-graph

# Web app
cd apps/web
yarn generate-codegen-dev
  1. Use generated hooks
import { useGetGameQuery } from './game.generated';

export function GameDetails({ id }) {
  const { data, loading } = useGetGameQuery({ variables: { id } });
  // ...
}

Best Practices

1. Co-locate Operations with Components

modules/games/
  game_details/
    game_details.tsx
    use_game/
      game.graphql
      game.generated.ts

2. Use Fragments for Reusability

fragment GameBasic on GameDTO {
  hltbId
  title
  coverUrl
}

query GetGame($id: Int!) {
  game(id: $id) {
    ...GameBasic
    description
    platforms
  }
}

query GetGames {
  games {
    ...GameBasic
  }
}

3. Proper Error Handling

const { data, error } = useGetGameQuery({ variables: { id } });

if (error) {
  if (error.message.includes('NOT_FOUND')) {
    return <NotFound />;
  }
  if (error.message.includes('UNAUTHORIZED')) {
    return <Unauthorized />;
  }
  return <ErrorMessage error={error} />;
}

4. Optimize with Field Selection

Only query what you need:

# Bad - fetching unnecessary data
query GetGames {
  games {
    hltbId
    title
    description
    platforms
    genres
    completionTime
    # ... everything
  }
}

# Good - only what's displayed
query GetGamesForList {
  games {
    hltbId
    title
    coverUrl
    rating
  }
}

5. Use Variables for Dynamic Queries

# Bad - hardcoded values
query GetGame {
  game(id: 123) {
    title
  }
}

# Good - use variables
query GetGame($id: Int!) {
  game(id: $id) {
    title
  }
}

6. Implement Proper Loading States

const { data, loading, error, refetch, networkStatus } = useGetGameQuery({
  variables: { id },
  notifyOnNetworkStatusChange: true,
});

const isRefetching = networkStatus === NetworkStatus.refetch;

if (loading && !isRefetching) return <Skeleton />;
if (error) return <Error />;
if (isRefetching) return <RefreshingIndicator />;

Common Patterns

Pagination

query GetGames($take: Int!, $skip: Int!) {
  games(take: $take, skip: $skip) {
    games {
      id
      title
    }
    total
  }
}

Infinite Scroll (Native)

const { data, fetchMore, loading } = useGetGamesQuery({
  variables: { take: 20, skip: 0 },
});

const loadMore = () => {
  fetchMore({
    variables: { skip: data.games.games.length },
    updateQuery: (prev, { fetchMoreResult }) => {
      return {
        games: {
          ...fetchMoreResult.games,
          games: [...prev.games.games, ...fetchMoreResult.games.games],
        },
      };
    },
  });
};

Search with Debouncing

const [search, setSearch] = useState('');
const [debouncedSearch] = useDebounce(search, 500);

const { data } = useSearchGamesQuery(
  { search: debouncedSearch },
  { enabled: debouncedSearch.length >= 3 }
);

Debugging

Enable GraphQL DevTools

Native:

// Apollo DevTools plugin already configured
import { __DEV__ } from 'react-native';

const client = new ApolloClient({
  // ...
  connectToDevTools: __DEV__,
});

Web:

// React Query DevTools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

<QueryClientProvider client={queryClient}>
  {children}
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>

Logging Queries

// API - enable playground
GraphQLModule.forRoot({
  playground: true,
  debug: true,
});

// Native - log all operations
const link = ApolloLink.from([
  new ApolloLink((operation, forward) => {
    console.log('GraphQL Operation:', operation.operationName);
    return forward(operation);
  }),
  httpLink,
]);

Code Generation Commands

# API - Schema auto-generates on start
cd apps/api
yarn dev

# Native - Generate from remote schema
cd apps/native
yarn generate-graph        # Local dev
yarn generate-graph-prod   # Production

# Web - Generate from remote schema
cd apps/web
yarn generate-codegen-dev

Common Issues & Solutions

Issue: Generated types not found

Solution: Ensure code generation completed successfully and restart TypeScript server

Issue: Schema changes not reflected

Solution: Restart API server to regenerate schema, then re-run codegen in clients

Issue: Authentication errors

Solution: Check token is properly passed in headers for both Apollo Client and fetcher

Issue: Cache not updating

Solution: Use refetchQueries, invalidateQueries, or manual cache updates