Go back

Migrando koa-graphql para graphql-http

2025-05-18 21:24 - 2025-06-11 02:14 (c531ab6), 4 min read
#pt-br , #webdev , #typescript , #graphql

Table of Contents

Recentemente eu desenvolvi o desafio da Woovi com Koa.js e GraphQL (link do projeto). Uma das especificações opcionais era o uso do graphql-http, substituto do koa-graphql, que está presente no playground disponibilizado. No entanto, me deparei com alguns problemas iniciais: diversas funcionalidades não estavam presentes no graphql-http, visto que ele é apenas uma implementação do GraphQL sobre HTTP, isto é, é simples e minimalista.

Introdução - Por quê? E alguns problemas

Primeiro, por quê? Bom, principalmente, porque koa-graphql foi praticamente descontinuado e graphql-http deve ser utilizado ao invés disso. Tomei esta decisão baseada em uma discussão que inclusive recomendava o uso do yoga, uma alternativa mais completa para servidores graphql, no entanto, eu vi que seria completamente capaz de resolver apenas com graphql-http. Caso queira uma opção mais robusta, tente o Yoga.

Vamos aos problemas. Eu utilizava algumas funcionalidades interessantes já presentes no koa-graphql, como:

  • o uso de uma customErrorFn para retornar erros do servidor de uma maneira específica, além de logar no console em ambientes de teste e desenvolvimento
  • graphiql: um playground GraphQL para fazer queries e mutations de maneira intuitiva pelo navegador
  • alterar o context da requisição, já que o projeto armazenava o usuário caso estivesse logado (por meio de token JWT)

Migrando para o graphql-http

A primeira coisa a ser feita é remover koa-graphql completamente.

pnpm un koa-graphql

E adicionar graphql-http:

pnpm i graphql-http

Com isso, é começar a adaptar as funcionalidades.

Criando o handler

Seguindo o snippet que está presente no próprio README do graphql-http, para criar o handler é bem simples, é só importar o createHandler para o koa e passar o schema e o context:

import { createHandler } from 'graphql-http/lib/use/koa';
 
const graphqlHandler = createHandler({
  schema,
  context: async (ctx) => {
    const { user } = await getUser(ctx);
    return getContext({ ctx, user });
  },
});

Ok, o schema é a parte mais fácil, apenas passei o schema definido anteriormente. No entanto, tive alguns problemas com os tipos do context, especialmente porque a forma de conseguir as informações do header são diferentes.

Adaptando os tipos do context

No get-context.ts, criei o tipo que deveria receber para o contexto:

import type { Request } from 'graphql-http';
import type { IncomingMessage } from 'node:http';
import type { RequestContext } from 'graphql-http/lib/use/koa';
 
export type RequestGraphQLContext = Request<IncomingMessage, RequestContext>;

Agora para o get-user.ts, antes eu simplesmente pegava o valor de context.headers.authorization, mas agora a tipagem para o headers é completamente diferente:

  • é preciso utilizar context.headers.get
  • get pode ser uma função, uma string, ou conter array no seu valor

Portanto, criei uma função para validar corretamente, o get-graphql-http-headers.ts:

import { Request } from 'graphql-http';
import { RequestContext } from 'graphql-http/lib/use/koa';
import { IncomingMessage } from 'node:http';
 
type HeaderKeyType = 'authorization';
 
export function getGraphQLHttpHeaders(
  ctx: Request<IncomingMessage, RequestContext>,
  headerKey: HeaderKeyType,
) {
  if (typeof ctx.headers.get === 'function') {
    return ctx.headers.get(headerKey);
  }
 
  if (typeof ctx.headers === 'object') {
    const header = (
      ctx.headers as Record<string, string | string[] | undefined>
    )[headerKey];
    if (Array.isArray(header)) {
      return header[0] ?? null;
    } else if (typeof header === 'string') {
      return header;
    }
  }
 
  return null;
}

Agora, para conseguir o authToken, basta utilizar a função criada:

const authHeader = getGraphQLHttpHeaders(ctx, 'authorization');

Schema e Context prontos! Agora faltam as funcionalidades: graphiql e tratamento de erros.

Tratamento de erros

Agora ficou ainda mais simples! Não precisamos passar uma função para o handler novo, basta apenas adicionar um middleware!

// graphql error handling middleware
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    if (error instanceof GraphQLError) {
      if (logEnvironments.includes(config.NODE_ENV)) {
        console.log(error.message);
        console.log(error.locations);
        console.log(error.stack);
      }
 
      ctx.body = {
        errors: [
          {
            message: error.message,
            locations: error.locations,
            stack: config.NODE_ENV === 'development' ? error.stack : undefined,
          },
        ],
      };
    } else {
      throw error;
    }
  }
});

GraphiQL - playground

Aqui eu mudei um pouco a forma que o playground é acessado. Como ele é utilizado apenas em ambiente de desenvolvimento, resolvi utilizar o ruru e adicionar um script no package.json do server para acessá-lo. Para isto, ele pede que o pacote ruru seja instalado, este inclusive pode ser utilizado em qualquer endpoint graphql disponível!

npx ruru -SP -p 3001 -e http://localhost:3000/graphql
  • -p especifica a porta do playground
  • -e especifica a rota para o servidr graphql

Concluindo

Acabou! Com isso, poupei alguns KBs:

Comparação de tamanho entre os pacotes graphql-http e koa-graphql. O graphql-http possui 2,2 KB minificados e 1,0 KB quando compactado com Gzip. Já o koa-graphql tem 2987,3 KB minificados e 840,9 KB com Gzip. As informações são da fonte BundlePhobia, com links para GitHub e BundlePhobia disponíveis abaixo de cada pacote

E, principalmente, atualizei para uma lib mais atualizada.

No meu projeto no GitHub você pode visualizar exatamente como foi minha linha de pensamento visualizando a issue e o pull request. E outra: tem um bônus onde eu configurei o graphiql em um app react-router v7 (antigo Remix), vale a pena dar uma olhada!

Caso tenha alguma dúvida, não hesite em comentar ou me chamar em qualquer um dos meus links

Referencias