Skip to content

lynxtaa/awesome-graphql-client

Repository files navigation

Logo

CI/CD npm version Codecov

Awesome GraphQL Client

GraphQL Client with file upload support for NodeJS and browser

Features

  • GraphQL File Upload support
  • Works in browsers and NodeJS
  • Zero dependencies
  • Small size (around 2Kb gzipped)
  • Full Typescript support
  • Supports queries generated by graphql-tag
  • Supports GraphQL GET requests
  • Perfect for React apps in combination with react-query. See Next.js example

Install

npm install awesome-graphql-client

Quick Start

Browser

import { AwesomeGraphQLClient } from 'awesome-graphql-client'

const client = new AwesomeGraphQLClient({ endpoint: '/graphql' })

// Also query can be an output from graphql-tag (see examples below)
const GetUsers = `
  query getUsers {
    users {
      id
    }
  }
`

const UploadUserAvatar = `
  mutation uploadUserAvatar($userId: Int!, $file: Upload!) {
    updateUser(id: $userId, input: { avatar: $file }) {
      id
    }
  }
`

client
  .request(GetUsers)
  .then(data =>
    client.request(UploadUserAvatar, {
      id: data.users[0].id,
      file: document.querySelector('input#avatar').files[0],
    }),
  )
  .then(data => console.log(data.updateUser.id))
  .catch(error => console.log(error))

NodeJS

NodeJS 20

const { openAsBlob } = require('node:fs')
const { AwesomeGraphQLClient } = require('awesome-graphql-client')

const client = new AwesomeGraphQLClient({
  endpoint: 'http://localhost:8080/graphql',
})

// Also query can be an output from graphql-tag (see examples below)
const UploadUserAvatar = `
  mutation uploadUserAvatar($userId: Int!, $file: Upload!) {
    updateUser(id: $userId, input: { avatar: $file }) {
      id
    }
  }
`

const blob = await openAsBlob('./avatar.png')

client
  .request(UploadUserAvatar, { file: new File([blob], 'avatar.png'), userId: 10 })
  .then(data => console.log(data.updateUser.id))
  .catch(error => console.log(error))

NodeJS 18

const { createReadStream, statSync } = require('node:fs')
const path = require('node:path')
const { Readable } = require('node:stream')
const { AwesomeGraphQLClient } = require('awesome-graphql-client')

class StreamableFile extends Blob {
  constructor(filePath) {
    const { mtime, size } = statSync(filePath)

    super([])

    this.name = path.parse(filePath).base
    this.lastModified = mtime.getTime()
    this.#filePath = filePath

    Object.defineProperty(this, 'size', {
      value: size,
      writable: false,
    })
    Object.defineProperty(this, Symbol.toStringTag, {
      value: 'File',
      writable: false,
    })
  }

  stream() {
    return Readable.toWeb(createReadStream(this.#filePath))
  }
}

const client = new AwesomeGraphQLClient({
  endpoint: 'http://localhost:8080/graphql',
})

// Also query can be an output from graphql-tag (see examples below)
const UploadUserAvatar = `
  mutation uploadUserAvatar($userId: Int!, $file: Upload!) {
    updateUser(id: $userId, input: { avatar: $file }) {
      id
    }
  }
`

client
  .request(UploadUserAvatar, { file: new StreamableFile('./avatar.png'), userId: 10 })
  .then(data => console.log(data.updateUser.id))
  .catch(error => console.log(error))

Table of Contents

API

AwesomeGraphQLClient

Usage:

import { AwesomeGraphQLClient } from 'awesome-graphql-client'
const client = new AwesomeGraphQLClient(config)

config properties

  • endpoint: string - The URL to your GraphQL endpoint (required)
  • fetch: Function - Fetch polyfill
  • fetchOptions: object - Overrides for fetch options
  • FormData: object - FormData polyfill
  • formatQuery: function(query: any): string - Custom query formatter (see example)
  • onError: function(error: GraphQLRequestError | Error): void - Provided callback will be called before throwing an error (see example)
  • isFileUpload: function(value: unknown): boolean - Custom predicate function for checking if value is a file (see example)

client methods

  • client.setFetchOptions(fetchOptions: FetchOptions): Sets fetch options. See examples below
  • client.getFetchOptions(): Returns current fetch options
  • client.setEndpoint(): string: Sets a new GraphQL endpoint
  • client.getEndpoint(): string: Returns current GraphQL endpoint
  • client.request(query, variables?, fetchOptions?): Promise<data>: Sends GraphQL Request and returns data or throws an error
  • client.requestSafe(query, variables?, fetchOptions?): Promise<{ ok: true, data, response } | { ok: false, error, partialData }>: Sends GraphQL Request and returns object with 'ok: true', 'data' and 'response' or with 'ok: false', 'error' and 'partialData' fields. See examples below. Notice: this function never throws.

GraphQLRequestError

instance fields

  • message: string - Error message
  • query: string - GraphQL query
  • variables: string | undefined - GraphQL variables
  • response: Response - response returned from fetch
  • fieldErrors: GraphQLFieldError[] - GraphQL field errors

Examples

Typescript

interface getUser {
  user: { id: number; login: string } | null
}
interface getUserVariables {
  id: number
}

const query = `
  query getUser($id: Int!) {
    user {
      id
      login
    }
  }
`

const client = new AwesomeGraphQLClient({
  endpoint: 'http://localhost:3000/graphql',
})

client
  .request<getUser, getUserVariables>(query, { id: 10 })
  .then(data => console.log(data))
  .catch(error => console.log(error))

client.requestSafe<getUser, getUserVariables>(query, { id: 10 }).then(result => {
  if (!result.ok) {
    throw result.error
  }
  console.log(`Status ${result.response.status}`, `Data ${result.data.user}`)
})

Typescript with TypedDocumentNode (even better!)

You can generate types from queries by using GraphQL Code Generator with TypedDocumentNode plugin

# queries.graphql
query getUser($id: Int!) {
  user {
    id
    login
  }
}
// index.ts
import { TypedDocumentNode } from '@graphql-typed-document-node/core'
import { AwesomeGraphQLClient } from 'awesome-graphql-client'
import { print } from 'graphql/language/printer'

import { GetCharactersDocument } from './generated'

const gqlClient = new AwesomeGraphQLClient({
  endpoint: 'https://rickandmortyapi.com/graphql',
  formatQuery: (query: TypedDocumentNode) => print(query),
})

// AwesomeGraphQLClient will infer all types from the passed query automagically:
gqlClient
  .request(GetCharactersDocument, { name: 'Rick' })
  .then(data => console.log(data))
  .catch(error => console.log(error))

Check out full example at examples/typed-document-node

Error Logging

import { AwesomeGraphQLClient, GraphQLRequestError } from 'awesome-graphql-client'

const client = new AwesomeGraphQLClient({
  endpoint: '/graphql',
  onError(error) {
    if (error instanceof GraphQLRequestError) {
      console.error(error.message)
      console.groupCollapsed('Operation:')
      console.log({ query: error.query, variables: error.variables })
      console.groupEnd()
    } else {
      console.error(error)
    }
  },
})

GraphQL GET Requests

Internally it uses URLSearchParams API. Consider polyfilling URL standard for this feature to work in IE

client
  .request(query, variables, { method: 'GET' })
  .then(data => console.log(data))
  .catch(err => console.log(err))

GraphQL Tag

Approach #1: Use formatQuery

import { AwesomeGraphQLClient } from 'awesome-graphql-client'
import { DocumentNode } from 'graphql/language/ast'
import { print } from 'graphql/language/printer'
import gql from 'graphql-tag'

const client = new AwesomeGraphQLClient({
  endpoint: '/graphql',
  formatQuery: (query: DocumentNode | string) =>
    typeof query === 'string' ? query : print(query),
})

const query = gql`
  query me {
    me {
      login
    }
  }
`

client
  .request(query)
  .then(data => console.log(data))
  .catch(err => console.log(err))

Approach #2: Use fake graphql-tag

Recommended approach if you're using graphql-tag only for syntax highlighting and static analysis such as linting and types generation. It has less computational cost and makes overall smaller bundles. GraphQL fragments are supported too.

import { AwesomeGraphQLClient, gql } from 'awesome-graphql-client'

const client = new AwesomeGraphQLClient({ endpoint: '/graphql' })

const query = gql`
  query me {
    me {
      login
    }
  }
`

client
  .request(query)
  .then(data => console.log(data))
  .catch(err => console.log(err))

Approach #3: Use TypedDocumentNode instead

Perfect for Typescript projects. See example above

Cookies in NodeJS

const { AwesomeGraphQLClient } = require('awesome-graphql-client')
const fetchCookie = require('fetch-cookie')

const client = new AwesomeGraphQLClient({
  endpoint: 'http://localhost:8080/graphql',
  fetch: fetchCookie(globalThis.fetch),
})

Custom isFileUpload Predicate

const { AwesomeGraphQLClient, isFileUpload } = require('awesome-graphql-client')

const client = new AwesomeGraphQLClient({
  endpoint: 'http://localhost:8080/graphql',
  // By default File, Blob, Buffer, Promise and stream-like instances are considered as files.
  // You can expand this behaviour by adding a custom predicate
  isFileUpload: value => isFileUpload(value) || value instanceof MyCustomFile,
})

More Examples

https://github.com/lynxtaa/awesome-graphql-client/tree/master/examples