GraphQL: Serviços em larga escala com GraphQL Modules
Eduardo Rabelo
Posted on November 12, 2019
Créditos
- GraphQL Modules — Feature based GraphQL Modules at scale, escrito originalmente por Urigo
Hoje, temos o prazer de anunciar que estamos disponibilizando uma estrutura de código aberto que estamos usando nos últimos dois meses em produção, GraphQL Modules!
É mais um framework? bem, mais ou menos .. O GraphQL Modules é um conjunto de bibliotecas, estruturas e diretrizes extras em torno do incrível Apollo Server 2.0.
Você pode e deve usá-los como pacotes completamente separados, cada um é bom para diferentes casos de uso, mas todos juntos representam nossa filosofia atual e real de criar serviços em GraphQL em larga escala.
Gostaríamos muito de receber feedback da equipe Apollo e, se eles desejarem usar essas idéias e integrá-las ao Apollo Server, gostaríamos de contribuir. É por isso que o desenvolvemos como um conjunto de ferramentas independentes em um único monorepo.
O conceito básico por trás do GraphQL Modules é separar o servidor GraphQL em partes menores, reutilizáveis e baseadas em recursos.
Uma implementação básica e inicial de um servidor GraphQL geralmente inclui:
Uma implementação mais avançada geralmente usa um contexto para injetar coisas como modelos de dados, fontes de dados, buscadores, etc., como o Apollo Server 2.0 nos fornece:
Normalmente, para casos de uso simples, o exemplo acima serve.
Mas à medida que os aplicativos crescem, seus códigos e relacionamentos esquemáticos se tornam maiores e mais complexos, o que pode tornar a manutenção de esquemas algo difícil e angustiante de se trabalhar.
Olhando em estruturas mais antigas, o MVC adiciona algumas camadas após a camada dos resolvedores, mas a maioria implementa apenas camadas técnicas baseadas em separação: controladores, modelos etc.
Nós acreditamos que existe uma abordagem melhor para escrever seu esquema e implementação do GraphQL.
Acreditamos que você deve separar seu esquema GraphQL por módulos ou recursos e incluir qualquer coisa relacionada a uma parte específica do aplicativo em um "módulo" - que é apenas um diretório simples. Cada uma das bibliotecas do GraphQL Modules ajudaria você no processo gradual de fazer isso.
Os módulos estão sendo definidos pelo esquema do GraphQL - por isso, adotamos a abordagem “GraphQL First” liderada pela Apollo e a combinamos com as ferramentas de modularização clássicas para criar novas maneiras de escrever servidores GraphQL!
O conjunto de ferramentas do GraphQL Modules possui ferramentas para ajudar:
- Separação de Esquemas - declare seu esquema do GraphQL em partes menores, que você poderá mover e reutilizar posteriormente.
- Ferramentas projetadas para criar módulos independentes - cada módulo é completamente independente e testável e pode ser compartilhado com outros aplicativos ou até de código aberto, se necessário.
- Composição dos resolvedores - com GraphQL Modules, você pode escrever o seu resolvedor como desejar e deixar o aplicativo que hospeda o módulo agrupar os resolvedores e estendê-los. Isso é implementado com uma API básica de middleware, mas com maior flexibilidade. Isso significa que você pode, por exemplo, implementar todo o seu módulo sem conhecer o processo de autenticação do aplicativo e assumir que currentUser que será injetado pelo aplicativo.
- Um caminho claro e gradual - módulos muito simples e rápidos de arquivo único a módulos escaláveis de múltiplos arquivos, múltiplas equipes, múltiplos repositórios e múltiplos servidores.
- Uma estrutura escalável para seus servidores GraphQL - gerenciando várias equipes e recursos, vários micros-serviços e servidores.
Ferramentas mais avançadas, que você pode optar por incluir quando o esquema entrar em grande escala:
- Ponte de comunicação - Também permitimos o envio de mensagens personalizadas com payload entre os módulos - o que significa que você pode executar módulos em diferentes micros-serviços e interagir facilmente entre eles.
- Injeção de Dependência - Implemente seus resolvedores e, posteriormente, somente quando achar necessário, melhore a implementação deles, introduzindo gradualmente a injeção de dependência. Também é incluído um conjunto de ferramentas ricas para testes e mocks.
Um exemplo prático
No exemplo a seguir, você pode ver uma implementação prática para um servidor GraphQL Modules, com 2 módulos: User e Chat.
Cada módulo declara apenas a parte que é relevante para ele e estende os tipos do GraphQL declarados anteriormente.
Portanto, quando o módulo User é carregado, o type User é criado, e quando o módulo Chat é carregado, o type User está sendo estendido com mais campos.
Em server.js:
import { AppModule } from './modules/app-module';
import { ApolloServer } from 'apollo-server';
const { schema, context } = AppModule;
const server = new ApolloServer({
schema,
context,
introspection: true,
});
server.listen();
Em app-module.js:
import { GraphQLModule } from '@graphql-modules/core';
import { UserModule } from './user-module';
import { ChatModule } from './chat-module';
export const AppModule = new GraphQLModule({
imports: [
UserModule,
ChatModule,
],
});
Em chat-module.js:
import { GraphQLModule } from '@graphql-modules/core';
import gql from 'graphql-tag';
export const ChatModule = new GraphQLModule({
typeDefs: gql`
# Query é declarada novamente, adicionando apenas a parte relevante
type Query {
myChats: [Chat]
}
# User é declarado novamente, estendendo qualquer outro tipo de `User` carregado no `appModule`
type User {
chats: [Chat]
}
type Chat {
id: ID!
users: [User]
messages: [ChatMessage]
}
type ChatMessage {
id: ID!
content: String!
user: User!
}
`,
resolvers: {
Query: {
myChats: (root, args, { getChats, currentUser }) => getChats(currentUser),
},
User: {
// Este módulo implementa apenas a parte do `User` que ele adiciona
chats: (user, args, { getChats }) => getChats(user),
},
},
});
Em user-module.js:
import { GraphQLModule } from '@graphql-modules/core';
import gql from 'graphql-tag';
export const UserModule = new GraphQLModule({
typeDefs: gql`
type Query {
me: User
}
# Este é o User inicial, com apenas o mínimo de um objeto de usuário
type User {
id: ID!
username: String!
email: String!
}
`,
resolvers: {
Query: {
me: (root, args, { currentUser ) => currentUser,
},
User: {
id: user => user._id,
username: user => user.username,
email: user => user.email.address,
},
},
});
Você pode e deve adotar GraphQL Modules parte por parte e pode experimentar agora com o seu servidor GraphQL existente.
O que um "módulo" contém?
- Esquema (declaração de tipos) - cada módulo pode definir seu próprio esquema e estender outros tipos de esquema (sem fornecê-los explicitamente).
- Implementação de resolvedores isolados - cada módulo pode implementar seus próprios resolvedores, resultando em resolvedores pequenos e isolados, ao invés de arquivos gigantes.
- Provedores - cada módulo pode ter seus próprios provedores, que são apenas classes / valores / funções que você pode usar dos seus resolvedores. Os módulos podem carregar e usar provedores de outros módulos.
- Configuração - cada módulo pode declarar um objeto de configuração fortemente tipado, que o aplicativo consumidor pode fornecer.
- Dependências - os módulos podem depender de outros módulos (pelo nome ou pela instância do GraphQLModule, para que você possa criar facilmente uma dependência ambígua que posteriormente poderá ser alterada).
Bibliotecas do GraphQL Modules
O GraphQL Modules é construído como um kit de ferramentas, com as seguintes ferramentas, que você deve adotar individualmente e gradualmente:
@graphql-modules/epoxy
- Essa provavelmente será a primeira ferramenta que você deseja introduzir no seu servidor. O primeiro passo para organizar seu servidor em uma estrutura baseada em recursos
- Epoxy é um pequeno utilitário que gerencia a mesclagem de esquema. permite mesclar tudo em seu esquema, começando com tipos até enumerações, uniões, diretivas e assim por diante.
- Esse é um recurso importante do GraphQL Modules - você pode usá-lo para separar seus tipos GraphQL em partes menores e, posteriormente, combiná-los em um único tipo.
- Tiramos a inspiração do merge-graphql-schemas e adicionamos alguns recursos para permitir regras de mesclagem personalizadas para facilitar a separação do esquema.
@graphql-modules/core
- Composição dos resolvedores - gerencia a empacota os resolvedores do aplicativo
- Construção de contexto - cada módulo pode injetar propriedades personalizadas no esquema e outros módulos podem usá-lo (por exemplo, o módulo auth pode injetar o usuário atual e outros módulos podem usá-lo)
- Injeção de dependência e gerenciamento de dependências do módulo - quando você inicia, não há necessidade de usar o DI em seu servidor, mas quando seu servidor fica grande o suficiente com um grande número de módulos que depende um do outro, somente então o DI se torna uma coisa de grande ajuda que realmente simplifica bastante o seu código. USE SOMENTE QUANDO NECESSÁRIO ;)
Você pode encontrar mais ferramentas à sua disposição, como:
- @graphql-modules/sonar - um pequeno utilitário que ajuda você a encontrar arquivos de esquemas e resolvedores do GraphQL e incluí-los.
- @graphql-modules/logger - um pequeno logger, baseado no winston 3, que você pode usar facilmente em seu aplicativo.
Passo a Passo
Primeira coisa, comece simples! Comece movendo seu código para pastas e estruturas baseadas em recursos com as ferramentas existentes.
Em seguida, acesse https://graphql-modules.com/ e confira nossas ferramentas e use-as somente quando perceber que elas resolvem um problema real para você!
Verifique também o README do repositório e vários aplicativos de exemplo.
Você provavelmente tem muitas perguntas - como isso se compara a outras ferramentas, como usar essas bibliotecas com o X e assim por diante.
Nas próximas semanas, publicaremos uma série de postagens que abordarão profundamente cada uma das decisões de design tomadas. Por isso, queremos ouvir seus pensamentos e perguntas. Por favor, comente aqui ou no repositório do Github!
Indo para o GraphQL Summit? Eu estarei lá e gostaria de responder suas perguntas e comentários em nome da nossa equipe.
Todas essas ferramentas foram construídas por um grupo apaixonado de desenvolvedores individuais de código aberto, também conhecido como The Guild.
Abaixo, há uma seção de pensamentos mais aprofundados sobre os quais publicaremos artigos separados nas próximas semanas:
Conceitos principais e um mergulho avançado
Modularizando um esquema
Todo mundo está falando sobre costurar esquemas (schema stitching) e GraphQL Bindings. Onde isso se encaixa na imagem?
A costura de esquema é uma habilidade e conceito incrível, que ajuda a mesclar servidores GraphQL separados em um único endpoint e abre muitos casos de uso interessantes.
Mas, com toda a empolgação, perdemos algo muito mais central do que isso - às vezes ainda queremos trabalhar em um único servidor lógico, mas queremos apenas separar o código de acordo com os recursos.
Queremos poder fazer a maior parte do trabalho de mesclagem no momento da construção e, somente se realmente necessário, fazer o restante da mesclagem no tempo de execução como último recurso.
Queremos dividir o código em equipes separadas e até criar módulos reutilizáveis que definem suas APIs externas por um esquema do GraphQL.
Esses módulos podem ser módulos npm, micros-serviços ou apenas pastas separadas dentro de um único servidor.
Separar seu esquema em partes menores é mais fácil quando você está lidando com typeDefs e resolvers, ficando mais legível e fácil de entender.
Também queríamos permitir que os desenvolvedores estendessem apenas tipos específicos, sem criar o esquema inteiro. Com o esquema do GraphQL, você precisa especificar pelo menos um campo no tipo Query, o que é algo que não queremos impor aos nossos usuários.
Vemos nossa abordagem como complementar ao Schema Stitching e trabalha em conjunto com ela.
Implementação baseada em recursos
Uma das coisas mais importantes na abordagem do GraphQL Modules é a implementação baseada em recursos.
Atualmente, a maioria das estruturas separa as camadas com base no papel da camada - como controladores, acesso a dados e assim por diante.
Os módulos do GraphQL Modules têm uma abordagem diferente - separa os módulos com base nos recursos do serviço e permite gerenciar suas próprias camadas em cada implementação de módulo.
É mais fácil pensar em aplicativos de maneira modular, por exemplo:
Seu incrível aplicativo precisa de autenticação, gerenciamento de usuários, perfis de usuário, galerias de usuários e bate-papo.
Cada um deles pode ser um módulo e pode implementar seu próprio esquema GraphQL e sua própria lógica, e pode depender de outros módulos para fornecer parte dessa lógica.
Aqui está um exemplo para um esquema do GraphQL, conforme descrito:
Mas se pensarmos nos aplicativos em termos de recursos e depois separarmos o esquema por módulo, a separação dos módulos terá a seguinte aparência:
Dessa forma, cada módulo pode declarar apenas a parte do esquema que ele contribui, e o esquema completo é uma representação de todas as definições de tipos mesclado.
O módulo também pode depender, importar, estender e personalizar o conteúdo de outros módulos (por exemplo, o módulo User vem com um Auth nele).
O resultado, é claro, será o mesmo, porque estamos mesclando o esquema em um único, mas a base de código será muito mais organizada e cada módulo terá sua própria lógica.
Reutilização de módulos do backend
Portanto, agora que entendemos o poder da implementação baseada em recursos, é mais fácil entender a ideia por trás da reutilização de código.
Se pudéssemos implementar o esquema e a parte principal do módulo Auth e User como "plug-and-play" - poderemos importá-lo posteriormente em outros projetos, com alterações muito pequenas (usando configuração, injeção de dependência ou composição de módulo).
Como poderíamos reutilizar módulos completos que contêm parte de um esquema?
Por exemplo, vamos usar um tipo User.
A maioria dos esquemas do tipo User conterá campos id, email e username. O tipo Mutation terá login e Query terá o campo user para consultar um usuário específico.
Podemos reutilizar essa declaração de tipo.
A implementação real pode diferir entre aplicativos, de acordo com o provedor de autenticação, o banco de dados e assim por diante, mas ainda podemos implementar a lógica de negócios em um resolvedor, usar o injetor de dependência e solicitar ao aplicativo que use o módulo que forneça a função de autenticação real (é claro, com uma interface TypeScript completa, saberemos que precisamos fornecê-la ;) ).
Vamos dar um passo adiante. Se desejarmos adicionar uma imagem de perfil a um usuário, podemos adicionar um novo módulo nomeado UserProfile e declarar novamente os tipos User e Mutation:
type User {
profilePicture: String
}
type Mutation {
uploadProfilePicture(image: File!): User
}
Dessa forma, o GraphQL Modules mesclará os campos desse tipo User no tipo completo User, e este módulo somente estenderá o tipo User e o tipo Mutation com as ações necessárias.
Então, digamos que temos o esquema - como podemos tornar esse módulo genérico e reutilizá-lo?
É assim que você declara este módulo:
import { GraphQLModule } from '@graphql-modules/core';
import gql from 'graphql-tag';
import { UserModule } from '../user';
import { Users } from '../user/users.provider';
export interface IUserProfileModuleConfig {
profilePictureFields ?: string;
uploadProfilePicture: (stream: Readable) => Promise<string>;
}
export const UserProfileModule = new GraphQLModule<IUserProfileModuleConfig>({
imports: [
UserModule,
],
typeDefs: gql`
type User {
profilePicture: String
}
type Mutation {
uploadProfilePicture(image: File!): User
}
`,
resolvers: config => ({
User: {
profilePicture: (user: User, args: never, context: ModuleContext) => {
const fieldName = config.profilePictureField || 'profilePic';
return user[fieldName] || null;
},
},
Mutation: {
uploadProfilePicture: async (root: never, { image }: { image: any }, { injector, currentUser }: ModuleContext) => {
// usando https://www.apollographql.com/docs/guides/file-uploads.html
const { stream } = await image;
// Obtenha o método externo para fazer upload de arquivos, isso é fornecido pelo aplicativo como config
const imageUrl = config.uploadProfilePicture(stream);
// Obtém o nome do campo
const fieldName = config.profilePictureField || 'profilePic';
// Peça ao injetor o token "Users", estamos assumindo que o módulo `user` o expõe para nós,
// e atualize o usuário com a URL enviado.
injector.get(Users).updateUser(currentUser, { [fieldName]: imageUrl });
// Retorne o usuário atual, podemos assumir que `currentUser` estará no contexto devido
// a composição dos resolvedores - explicaremos mais adiante.
return currentUser;
},
},
}),
});
Declaramos um objeto de configuração e o aplicativo o fornecerá para que possamos substituí-lo posteriormente por uma lógica diferente para o upload.
Escalando a base de código
Agora que dividimos nosso aplicativo em módulos individuais, ao modo que nossa base de código cresce, podemos dimensionar cada módulo individualmente.
O que quero dizer com dimensionar uma base de código?
Digamos que começamos a ter partes de código que queremos compartilhar entre diferentes módulos.
A maneira atual de fazer isso no mundo existente do GraphQL é através de um contexto do GraphQL.
Essa abordagem provou funcionar, mas em algum momento se torna um grande aborrecimento para manter, porque o contexto do GraphQL é um objeto que qualquer parte do aplicativo pode modificar, editar e estender, e pode se tornar realmente grande rapidamente.
O GraphQL Modules permitem que cada módulo estenda e injete campos no objeto context, mas isso é algo que você deve usar com cuidado, porque eu recomendo que o context contenha o context real - que contém dados como configuração global, ambiente, o usuário atual e assim por diante.
O GraphQL Modules adiciona apenas um campo no context, chamado de injector, que é a ponte que permite acessar o GraphQLApp e o aplicativo Injector, além de buscar a configuração e os provedores do módulo.
Os módulos podem ser um diretório simples em um projeto ou em um monorepo ou um módulo NPM publicado - você tem o poder de escolher como gerenciar sua base de código de acordo com suas necessidades e preferências.
Injeção de dependência
A injeção de dependência do GraphQL Modules é inspirada na injeção de dependência do .NET e Java, que provou funcionar ao longo dos anos. Com isso dito, houve alguns problemas com as APIs do .NET e Java, que tentamos listar e analisar. Tivemos algumas conclusões bastante interessantes.
Aprendemos que não é algo que deva ser forçado. A injeção de dependência faz sentido em alguns casos de uso específicos e você deve usá-lo somente quando for necessário. Portanto, esse conceito deve ser cada vez mais útil à medida que aumentamos a escala, podemos simplificar as coisas, manter nosso código com facilidade e gerenciar as contribuições de nossas equipes!
Estamos utilizando GraphQL Modules em todos os nossos clientes corporativos, enquanto também usamos em nossos aplicativos menores, nos leva a acreditar que encontramos o ponto ideal de onde você deve usar o conceito de injeção de dependência e quando não.
Também achamos uma API ideal para injeção de dependência. É extremamente fácil de entender e usar.
Após uma longa pesquisa sobre as soluções de injeção de dependência existentes para JavaScript, decidimos implementar um Injector simples, que suporta as necessidades do ecossistema do GraphQL Modules, suporta dependências circulares e muito mais.
Simplificamos a API de injeção de dependência e expusemos a você apenas as partes importantes que acreditamos serem necessárias para o desenvolvimento de serviços GraphQL.
Autenticação
Confira a postagem que escrevemos sobre isso:
Testes e Stubs
Em nossos aplicativos, quando começamos a usar a injeção de dependência, não era mais necessário gerenciar instâncias e uni-las.
Ganhamos uma abstração que nos permitiu testar as coisas com mais facilidade e mocks de todas as solicitações de http.
Sim, mocks. DI realmente brilha aqui.
Graças à mocks, podemos simular muitos cenários e comparar o backend com eles.
E quando a sua base de código cresce, você precisa começar a pensar em gerenciar dependências entre módulos e em como evitar coisas como dependências circulares - a menos que você use o DI que resolve esse problema para você.
Com o poder da injeção de dependência, você pode criar facilmente uma conexão entre os módulos e basear essa conexão em um token e em uma interface TypeScript.
Isso também significa que o teste é muito mais fácil - você pode pegar sua classe / função e testá-lo como uma unidade independente e mocks de suas dependências facilmente.
Finalizando
Entendemos como a estrutura do GraphQL Modules funciona, como ela foi construída a partir do zero, com os novos e empolgantes recursos do GraphQL e Apollo, combinando-os da maneira correta com as boas e antigas práticas recomendadas de software para dimensionar com modularizações, tipificações fortes e injeção de dependência.
Agora vá e experimente - https://graphql-modules.com/
Todos os artigos sbore GraphQL Modules
- GraphQL Modules — Feature based GraphQL Modules at scale
- Why is True Modular Encapsulation So Important in Large-Scale GraphQL Projects?
- Why did we implement our own Dependency Injection library for GraphQL-Modules?
- Scoped Providers in GraphQL-Modules Dependency Injection
- Writing a GraphQL TypeScript project w/ GraphQL-Modules and GraphQL-Code-Generator
- Authentication and Authorization in GraphQL (and how GraphQL-Modules can help)
- Authentication with AccountsJS & GraphQL Modules
- Manage Circular Imports Hell with GraphQL-Modules
Siga-nos no GitHub e no Medium. Planejamos lançar muitas outras postagens nas próximas semanas sobre o que aprendemos usando o GraphQL nos últimos anos.
Posted on November 12, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.