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 delogar
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:
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
- Meu projeto no GitHub: https://github.com/gustav0d/wbonk
- https://github.com/graphql/graphql-http/discussions/95
Foto de Mohammad Rahmani na Unsplash