Zum Inhalt springen

Migrando koa-graphql para grahpql-http

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 criar 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 pode ser 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

Foto de Mohammad Rahmani na Unsplash

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert