Loading...
Loading...
Generate TypeScript types from Quiltt's GraphQL schema for type-safe queries, mutations, and subscriptions with full IDE autocompletion.
Time: ~20 minutes Level: Intermediate Requirements: TypeScript, GraphQL basics
GraphQL Code Generator provides:
Add GraphQL Code Generator packages:
pnpm add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
pnpm add graphql @quiltt/react
Add your API credentials:
# .env.local
QUILTT_API_KEY_SECRET=your_api_key_secret_here
NEXT_PUBLIC_QUILTT_CLIENT_ID=your_client_id_here
Add codegen.ts in your project root:
// codegen.ts
import { CodegenConfig } from '@graphql-codegen/cli'
import * as dotenv from 'dotenv'
// Load environment variables
dotenv.config({ path: '.env.local' })
const config: CodegenConfig = {
overwrite: true,
// Schema configuration
schema: [
{
'https://api.quiltt.io/v1/graphql': {
headers: {
Authorization: `Bearer ${process.env.QUILTT_API_KEY_SECRET}`,
},
},
},
],
// Source files containing GraphQL operations
documents: ['src/**/*.tsx', 'src/**/*.ts'],
// Output configuration
generates: {
'src/types/generated/': {
preset: 'client',
plugins: [],
presetConfig: {
gqlTagName: 'gql',
fragmentMasking: false,
},
config: {
enumsAsTypes: true,
dedupeFragments: true,
skipTypename: false,
},
},
},
// Watch mode configuration
watch: true,
// Error handling
hooks: {
afterOneFileWrite: ['prettier --write'],
},
}
export default config
What this does:
.ts/.tsx files for GraphQL operationssrc/types/generated/Update package.json:
{
"scripts": {
"codegen": "graphql-codegen",
"codegen:watch": "graphql-codegen -w",
"dev": "pnpm codegen:watch & next dev"
}
}
Create a component with type-safe queries:
// src/components/AccountsList.tsx
import { gql, useQuery } from '@quiltt/react'
import type { Account } from '@/types/generated/graphql'
// This query will be type-checked
const GET_ACCOUNTS = gql`
query GetAccounts($connectionId: ID!) {
connection(id: $connectionId) {
accounts {
id
name
balance
type
}
}
}
`
interface AccountsListProps {
connectionId: string
}
export default function AccountsList({ connectionId }: AccountsListProps) {
// Types for data and variables are inferred automatically
const { data, loading, error } = useQuery(GET_ACCOUNTS, {
variables: { connectionId },
})
if (loading) return <div>Loading accounts...</div>
if (error) return <div>Error: {error.message}</div>
// TypeScript knows the shape of data.connection.accounts
return (
<div className="space-y-4">
{data?.connection?.accounts.map((account: Account) => (
<div key={account.id} className="rounded-lg bg-white p-4 shadow">
<h3 className="font-semibold">{account.name}</h3>
<p className="text-gray-600">${account.balance.toFixed(2)}</p>
<p className="text-gray-500 text-sm">{account.type}</p>
</div>
))}
</div>
)
}
Here's how to use generated types with mutations:
// src/components/ConnectionManager.tsx
import { gql, useMutation } from '@quiltt/react'
import type { Connection } from '@/types/generated/graphql'
const UPDATE_CONNECTION = gql`
mutation UpdateConnection($id: ID!, $input: ConnectionUpdateInput!) {
updateConnection(id: $id, input: $input) {
id
status
updatedAt
}
}
`
interface ConnectionManagerProps {
connection: Connection
}
export default function ConnectionManager({ connection }: ConnectionManagerProps) {
// Types for mutation variables and result are inferred
const [updateConnection, { loading }] = useMutation(UPDATE_CONNECTION)
const handleUpdate = async () => {
try {
const result = await updateConnection({
variables: {
id: connection.id,
input: {
status: 'SYNCING',
},
},
})
console.log('Updated connection:', result.data?.updateConnection)
} catch (error) {
console.error('Failed to update connection:', error)
}
}
return (
<button
onClick={handleUpdate}
disabled={loading}
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Updating...' : 'Update Connection'}
</button>
)
}
Here's how to implement type-safe subscriptions:
// src/components/AccountUpdates.tsx
import { gql, useSubscription } from '@quiltt/react'
import type { AccountUpdate } from '@/types/generated/graphql'
const ACCOUNT_UPDATES = gql`
subscription OnAccountUpdate($accountId: ID!) {
accountUpdate(accountId: $accountId) {
id
balance
updatedAt
}
}
`
interface AccountUpdatesProps {
accountId: string
onUpdate: (update: AccountUpdate) => void
}
export default function AccountUpdates({ accountId, onUpdate }: AccountUpdatesProps) {
useSubscription(ACCOUNT_UPDATES, {
variables: { accountId },
onData: ({ data }) => {
if (data?.accountUpdate) {
onUpdate(data.accountUpdate)
}
},
})
return null // This is a monitoring component with no UI
}
Use fragments to share common fields between queries:
// src/graphql/fragments.ts
import { gql } from '@quiltt/react'
export const ACCOUNT_FIELDS = gql`
fragment AccountFields on Account {
id
name
balance
type
status
updatedAt
}
`
// Using the fragment
const GET_ACCOUNTS = gql`
query GetAccounts($connectionId: ID!) {
connection(id: $connectionId) {
accounts {
...AccountFields
}
}
}
${ACCOUNT_FIELDS}
`
Create type-safe custom hooks:
// src/hooks/useAccount.ts
import { gql, useQuery } from '@quiltt/react'
import type { Account } from '@/types/generated/graphql'
const GET_ACCOUNT = gql`
query GetAccount($id: ID!) {
account(id: $id) {
...AccountFields
}
}
${ACCOUNT_FIELDS}
`
export function useAccount(id: string) {
return useQuery<{ account: Account }>(GET_ACCOUNT, {
variables: { id },
})
}
Create type-safe error handling utilities:
// src/utils/error-handling.ts
import { CombinedGraphQLErrors } from '@quiltt/react'
import type { GraphQLError } from 'graphql'
interface ErrorHandlerOptions {
defaultMessage?: string
logError?: boolean
}
export function handleGraphQLError(
error: GraphQLError | Error | unknown,
options: ErrorHandlerOptions = {}
) {
const { defaultMessage = 'An error occurred', logError = true } = options
if (logError) {
console.error('GraphQL Error:', error)
}
if (CombinedGraphQLErrors.is(error)) {
return error.errors[0]?.message || defaultMessage
}
if (error instanceof GraphQLError) {
return error.message || defaultMessage
}
if (error instanceof Error) {
return error.message || defaultMessage
}
return defaultMessage
}
If you're having trouble loading the schema:
// Manual schema loading script
// scripts/fetch-schema.ts
import { introspectSchema } from '@graphql-tools/wrap'
import { print } from 'graphql'
import fs from 'fs'
import path from 'path'
async function fetchSchema() {
try {
const schema = await introspectSchema({
async fetch(operation) {
const response = await fetch('https://api.quiltt.io/v1/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.QUILTT_API_KEY_SECRET}`,
},
body: JSON.stringify({ query: print(operation) }),
})
return response.json()
},
})
fs.writeFileSync(
path.join(__dirname, '../schema.graphql'),
print(schema)
)
console.log('Schema downloaded successfully')
} catch (error) {
console.error('Failed to fetch schema:', error)
}
}
fetchSchema()
If you're experiencing generation problems:
rm -rf src/types/generated
// src/utils/validate-operations.ts
import { validate } from 'graphql'
import { loadSchema } from '@graphql-tools/load'
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'
import glob from 'glob'
async function validateOperations() {
const schema = await loadSchema('schema.graphql', {
loaders: [new GraphQLFileLoader()],
})
const files = glob.sync('src/**/*.{ts,tsx}')
let hasErrors = false
for (const file of files) {
const content = require(file)
if (content.document) {
const errors = validate(schema, content.document)
if (errors.length > 0) {
console.error(`Validation errors in ${file}:`, errors)
hasErrors = true
}
}
}
return !hasErrors
}
validateOperations()
If you're having problems with type resolution:
// src/types/graphql.d.ts
declare module '*/graphql' {
import { DocumentNode } from 'graphql'
const value: DocumentNode
export = value
}
Configure custom scalar types:
// codegen.ts
const config: CodegenConfig = {
// ... other config
config: {
scalars: {
DateTime: 'string',
JSON: '{ [key: string]: any }',
UUID: 'string',
},
},
}
Create operation presets for common queries:
// src/graphql/operations.ts
import { gql } from '@quiltt/react'
import type { TypedDocumentNode } from '@graphql-typed-document-node/core'
import type { Account, Connection } from '@/types/generated/graphql'
export const accountOperations = {
getAccount: gql`
query GetAccount($id: ID!) {
account(id: $id) {
...AccountFields
}
}
${ACCOUNT_FIELDS}
` as TypedDocumentNode<{ account: Account }, { id: string }>,
// Add more account operations...
}
export const connectionOperations = {
// Connection operations...
}
Set up a development workflow that automatically generates types:
// scripts/watch-and-generate.ts
import { spawn } from 'child_process'
import chokidar from 'chokidar'
// Watch for changes in GraphQL files
chokidar.watch(['src/**/*.graphql', 'src/**/*.tsx']).on('change', (path) => {
console.log(`File ${path} changed`)
// Run codegen
const codegen = spawn('pnpm', ['codegen'])
codegen.stdout.on('data', (data) => {
console.log(`codegen: ${data}`)
})
codegen.stderr.on('data', (data) => {
console.error(`codegen error: ${data}`)
})
})
By following this guide, you've set up a robust GraphQL development environment with:
Remember to:
Continue Learning:
Related Tutorials:
Reference Documentation: