GraphQL Avançado: Criando uma API de Alto Desempenho com Node.js, Apollo Server e Prisma
GraphQL Avançado: Criando uma API de Alto Desempenho com Node.js, Apollo Server e Prisma
Para criar APIs modernas e escaláveis, muitas equipes estão migrando de REST para GraphQL. Com GraphQL, o cliente pode solicitar apenas os dados necessários, usando uma única rota de API, o que reduz over-fetching e under-fetching. Neste tutorial prático, você vai aprender como montar do zero uma API GraphQL de alto desempenho usando Node.js, Apollo Server e Prisma. Vamos abordar desde a configuração inicial do projeto até otimizações de performance em produção, incluindo exemplos de consultas e mutações.
Ao final deste artigo, você terá construído uma API GraphQL completa, com schema, resolvers e integração com banco de dados via Prisma, além de entender técnicas para evitar gargalos de desempenho comuns (como o problema N+1). Prepare seu editor: vamos codar uma API GraphQL escalável!
Por que GraphQL, Apollo Server e Prisma?
Antes de mergulharmos na prática, vale entender por que usar GraphQL com Node.js, Apollo Server e Prisma. Cada combinação traz vantagens:
-
GraphQL: permite ao cliente especificar exatamente quais campos quer na resposta. É como um cardápio em que você escolhe apenas os itens que irá consumir. Isso elimina over-fetching (buscar dados desnecessários) e under-fetching (precisar de várias requisições) comuns em APIs REST tradicionais. Além disso, GraphQL oferece tipagem forte e introspecção do esquema, facilitando desenvolvimento e validação de consultas.
-
Node.js: ambiente de execução JavaScript muito usado para servidores. É rápido, baseado em evento, escalável e tem um ecossistema rico. No nosso caso, usaremos Node para rodar o servidor GraphQL.
-
Apollo Server: é uma das soluções de servidor GraphQL mais populares. É open source, fácil de configurar, oferece integração com diversos bancos e frameworks, e possui ótimo suporte a recursos avançados (como plugins de performance e canais de administração).
-
Prisma: é um ORM moderno (Object-Relational Mapping) que gera um cliente de banco de dados tipado para Node.js. Com Prisma, você define o modelo de dados em um esquema (formato similar ao GraphQL) e gera automaticamente um cliente para fazer consultas de forma segura e produtiva. Isso acelera a escrita de queries e mutations no banco.
Em resumo, usar GraphQL + Apollo Server + Prisma resulta em um fluxo de trabalho produtivo e seguro: o GraphQL define as operações de API, o Apollo expõe o servidor, e o Prisma cuida das interações com o banco. Agora, vamos configurar o ambiente e começar a criar nossa API.
Configurando o ambiente e dependências
Primeiro, vamos preparar um projeto Node.js para nossa API.
-
Crie uma nova pasta para o projeto e entre nela:
mkdir minha-api-graphql cd minha-api-graphql -
Inicialize um projeto Node.js e responda às perguntas do
npm init. Para pular, pode usar-y:npm init -y -
Instale as dependências principais: o Apollo Server, GraphQL e o Prisma. No terminal, execute:
npm install apollo-server graphql prisma @prisma/clientIsso instala:
- apollo-server: biblioteca principal para criar o servidor GraphQL.
- graphql: pacote base do GraphQL.
- prisma: ferramenta de linha de comando do Prisma.
- @prisma/client: client gerado para interagir com o banco de dados.
-
Inicie o Prisma para configurar o acesso ao banco. Por padrão, o Prisma já sugere usar SQLite num projeto recém-criado. Execute:
npx prisma initIsso criará uma pasta
prisma/com o arquivoschema.prismae um arquivo.env. Você pode configurar o banco ajustandoschema.prismaou o.env. Para seguir este tutorial, aceitaremos o SQLite padrão (o arquivo SQLite será criado emdev.db). O conteúdo inicial doschema.prismaserá parecido com:datasource db { provider = "sqlite" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } -
(Opcional) Caso queira usar outro banco como PostgreSQL, edite o
providere aurlemschema.prisma/.env, mas lembre-se de ter o banco instalado.
Agora o ambiente básico está pronto. No próximo passo, vamos definir o esquema GraphQL (types, queries, mutations) e implementar os resolvers que vão usar o banco via Prisma.
Definindo o esquema GraphQL e implementando resolvers
Com o ambiente configurado, podemos criar o servidor GraphQL. Vamos definir primeiro o esquema (schema) usando a linguagem de definição de esquema (SDL) do GraphQL, em seguida escrever resolvers para as operações definidas, ligando-os ao Prisma Client.
Escrevendo o esquema (typeDefs)
Crie um arquivo index.js na raiz do projeto. Nele, vamos importar o Apollo Server e definir o esquema. Por exemplo, suponha que nossa API gerencia usuários (User) e postagens (Post). Podemos definir no tipo typeDefs:
const { ApolloServer, gql } = require('apollo-server'); const typeDefs = gql` type User { id: ID! name: String! email: String! posts: [Post!]! # lista de posts criados pelo usuário } type Post { id: ID! title: String! content: String author: User! # usuário que criou o post } type Query { users: [User!]! # obtém todos usuários posts: [Post!]! # obtém todos posts post(id: ID!): Post # obtém post por ID user(id: ID!): User # obtém usuário por ID } type Mutation { createUser(name: String!, email: String!): User! createPost(title: String!, content: String, authorId: ID!): Post! deletePost(id: ID!): Post } `;
Explicando o esquema acima:
- Tipos (Types): Temos
UserePost, com campos relacionados (um usuário tem vários posts e cada post tem um usuário autor). - Query (Consultas): Permite consultas
users,posts,post(id),user(id). - Mutation: Permite criar usuário (
createUser), criar post e deletar post. Cada mutation retorna o objeto criado ou removido.
Note que usamos [Post!]! para dizer que o campo posts retorna uma lista não nula de objetos Post, e cada Post na lista também não é nulo. Essa sintaxe ensina ao cliente e ao servidor o tipo e obrigatoriedade dos campos.
Implementando resolvers
Agora, precisamos implementar as funções chamadas resolvers, que realmente pegam/alteram dados. Crie um objeto resolvers no mesmo index.js. Vamos ligar o Prisma Client dentro dos resolvers usando o contexto:
const { ApolloServer, gql } = require('apollo-server'); const { PrismaClient } = require('@prisma/client'); // Cria uma instância do Prisma Client const prisma = new PrismaClient(); const typeDefs = gql` ...schema aqui (cole o tipo definido acima)... `; const resolvers = { Query: { users: () => prisma.user.findMany(), posts: () => prisma.post.findMany(), post: (_, { id }) => prisma.post.findUnique({ where: { id: Number(id) } }), user: (_, { id }) => prisma.user.findUnique({ where: { id: Number(id) } }), }, Mutation: { createUser: (_, { name, email }) => { return prisma.user.create({ data: { name, email } }); }, createPost: (_, { title, content, authorId }) => { return prisma.post.create({ data: { title, content, author: { connect: { id: Number(authorId) } } } }); }, deletePost: (_, { id }) => { return prisma.post.delete({ where: { id: Number(id) } }); }, }, // Resolvers para campos adicionais na tipagem User: { posts: (parent) => { // Retornar todos os posts com authorId igual ao ID do usuário return prisma.post.findMany({ where: { authorId: parent.id } }); }, }, Post: { author: (parent) => { // Retornar o usuário criador do post return prisma.user.findUnique({ where: { id: parent.authorId } }); }, }, };
Neste código:
- Para cada operação em
QueryeMutation, usamos métodos doprismapara consultar o banco. Por exemplo,prisma.user.findMany()retorna todos usuários. - Em
User.postsePost.author, criamos resolvers de campo para buscar relacionamentos entre modelos. - Note que convertemos
idpara número (Number(id)), pois no schema do Prismaidé do tipoInt.
Para finalizar, criamos o servidor Apollo e o colocamos para ouvir em uma porta. Ainda em index.js:
const server = new ApolloServer({ typeDefs, resolvers, context: () => ({ prisma }) // disponibiliza o Prisma Client no contexto dos resolvers }); server.listen().then(({ url }) => { console.log(`Servidor executando em: ${url}`); });
Quando rodarmos node index.js, o Apollo Server iniciará e mostrará uma URL (por padrão http://localhost:4000/). Através dessa URL você poderá acessar o GraphQL Playground ou outra ferramenta GraphQL para testar suas consultas (queries) e mutações.
Integração com o Prisma e manipulação dos dados
Nos passos anteriores, vimos rapidamente o Prisma Client em ação. Agora vamos detalhar como o Prisma funciona por baixo dos panos para armazenar e recuperar dados.
Modelagem de dados com o Prisma
O arquivo schema.prisma define seu modelo de dados (entities e relacionamentos). Um exemplo completo baseado nos tipos usados acima seria:
datasource db { provider = "sqlite" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } model User { id Int @id @default(autoincrement()) name String email String @unique posts Post[] // Um-para-Muitos: um usuário tem vários posts } model Post { id Int @id @default(autoincrement()) title String content String? author User @relation(fields: [authorId], references: [id]) authorId Int }
Explicação:
- Cada
modelno Prisma corresponde a uma tabela no banco de dados. @id @default(autoincrement())indica chave primária auto-incremental.- Em
User,emailé único. - A relação
User -> Posté feita criando um campoposts: Post[]emUsere referenciandoauthorIdemPost. author User @relation(...)emPoste o campoauthorIddefinem a relação inversa.
Após definir esse esquema no prisma/schema.prisma, podemos gerar as tabelas e o client:
npx prisma migrate dev --name init
Este comando cria a migração, aplica no banco e gera o Prisma Client automaticamente. Agora o banco de dados possui as tabelas User e Post.
Usando o Prisma Client nos resolvers
Voltando ao nosso servidor GraphQL, usamos o Prisma Client (prisma.user, prisma.post) para manipular os modelos. Alguns métodos comuns:
prisma.user.findMany()retorna uma lista de usuários.prisma.user.findUnique({ where: { id: 1 } })retorna o usuário comid = 1.prisma.user.create({ data: { name, email } })cria um usuário.prisma.user.update({ where: { id }, data: { name: novoNome } })atualiza.prisma.user.delete({ where: { id } })exclui.
Exemplo de uso no resolver:
async function createPostResolver(_, { title, content, authorId }) { // Cria um post vinculado a um usuário já existente. return await prisma.post.create({ data: { title, content, author: { connect: { id: Number(authorId) } } } }); }
No caso acima, usamos connect para ligar o post ao usuário existente com id = authorId.
O Prisma carece de diálogos elaborados sobre como o banco funciona: basta chamar esses métodos no resolver. Assim, nossa camada de dados (persistência) fica limpa e poderosa.
Exemplos de consultas e mutações
Com o servidor rodando, podemos testar alguns exemplos práticos de GraphQL. No Playground, podemos escrever consultas e vê-las em ação. Por exemplo:
Consulta (Query) de usuários:
query { users { id name email posts { id title } } }
Esse query retorna todos usuários com seus id, name, email e lista de posts (cada post solicitando id e title). Internamente, para posts, será chamado o resolver User.posts.
Mutação para criar um usuário:
mutation { createUser(name: "Ana", email: "ana@example.com") { id name email } }
Isso cria um usuário novo e retorna seus dados. Em seguida, podemos criar um post:
mutation { createPost(title: "Olá Mundo", content: "Meu primeiro post!", authorId: 1) { id title author { name } } }
Aqui, authorId: 1 liga o post ao usuário de id = 1. Observe que retornamos o author do post (no resolver Post.author, usamos o user.findUnique para buscar o usuário associado).
Esses exemplos mostram como o GraphQL permite objetos aninhados: fazemos apenas uma requisição e recebemos dados relacionados. Entretanto, consultas aninhadas profundas podem causar overhead no servidor e no banco. No próximo tópico, veremos como otimizar esse cenário.
Otimizando desempenho e boas práticas de produção
Construir a API é apenas parte do trabalho: precisamos garantir que ela seja eficiente e escalável. A seguir, reunimos algumas práticas e técnicas para melhorar a performance de sua API GraphQL.
Evitando o problema N+1 com DataLoader
Um dos maiores vilões de performance em GraphQL é o problema N+1. Imagine a seguinte consulta:
query { posts { id title author { id name } } }
Para cada post retornado, se usamos o resolver Post.author que faz prisma.user.findUnique, teremos uma consulta ao banco para cada post individual. Se houver 10 posts, isso gera 10 consultas (mais 1 consulta inicial de posts), totalizando 11. Conforme o número de posts cresce, as consultas disparam linearmente, degradando o desempenho.
A solução elegível é usar o DataLoader (https://github.com/graphql/dataloader), uma ferramenta de batching e caching de requisições. Em vez de executar 10 consultas separadas, o DataLoader agrupa pedidos de usuários em uma única consulta findMany({ where: { id: [listaDeIds] } }), retornando todos de uma vez e relacionando aos posts.
Exemplo de uso do DataLoader no contexto:
const DataLoader = require('dataloader'); const userLoader = new DataLoader(async (ids) => { const users = await prisma.user.findMany({ where: { id: { in: ids.map(id => Number(id)) } }, }); // Garante que a ordem de retorno corresponda à ordem de 'ids' return ids.map(id => users.find(user => user.id === Number(id))); });
Depois, no servidor Apollo, passamos o loader pelo contexto:
const server = new ApolloServer({ typeDefs, resolvers, context: () => ({ prisma, userLoader }) });
E ajustamos o resolver Post.author para usar o loader:
Post: { author: (parent, args, context) => { return context.userLoader.load(parent.authorId); } }
Agora, se a consulta pedir 10 authors diferentes, o DataLoader fará apenas uma única query prisma.user.findMany com todos os authorId, em vez de 10 queries separadas. Isso reduz drasticamente as solicitações ao banco e melhora o tempo de resposta.
Caching de respostas e consultas frequentes
Outro recurso importante em APIs de produção é o cache. O Apollo Server suporta cache-control directives e plugins que armazenam resultados de queries em memória ou em um repositório (como Redis). Isso significa que, se a mesma query for feita repetidamente sem mudança de dados, você pode retornar a resposta armazenada sem consultar o banco novamente.
Por exemplo, usando @cacheControl no esquema:
type Post { id: ID! @cacheControl(maxAge: 60) title: String! content: String @cacheControl(maxAge: 30) author: User! }
Isso indica ao Apollo que a resposta de id e title de Post pode ser armazenada por 60 segundos. Também há plugins como ApolloServerPluginResponseCache que facilitam o cache de queries completas.
Além disso, persisted queries são uma técnica onde você envia somente um identificador (hash) em vez da query inteira, economizando banda e reforçando segurança. Embora o Apollo tenha suporte a isso (Apollo Engine), basta saber que consultas muito repetitivas podem ser cacheadas, trazendo respostas quase instantâneas.
Paginação e limites de profundidade
Para evitar que consultas muito grandes sobrecarreguem o servidor, é recomendável implementar paginação em listas grandes (posts, por exemplo). Em vez de retornar todos os posts de uma vez, crie queries que recebam parâmetros de paginação: first, skip, ou mesmo cursores (cursor-based pagination).
Também é possível usar bibliotecas de limitação de profundidade de query, como graphql-depth-limit, para proibir queries extremamente aninhadas que reclamam muitos recursos. Definir uma profundidade máxima razoável previne ataques ou consumos acidentais de CPU/memória.
Monitoramento, logging e ambiente de produção
Em produção, não esqueça de:
- Desabilitar ferramentas de desenvolvimento: por exemplo, o GraphQL Playground fica desabilitado em modo
productionpor padrão no Apollo. Só habilite se for necessário. - Logging detalhado: registre tempo de execução de resolvers, número de queries, para detectar gargalos. Você pode usar plugins do Apollo para saber a performance de cada query.
- VMs e cluster: rode múltiplas instâncias do servidor (por exemplo, em um servidor Node cluster ou behind de um load balancer) para suportar alto tráfego.
- Variáveis de ambiente: use o
NODE_ENV=productione configure corretamente as strings de conexão do banco, endereços e chaves secretas.
Em resumo, combine DataLoader, caching, paginação e boas práticas de segurança (autenticação/autorização em mutations, por exemplo) para tornar sua API GraphQL robusta.
Conclusão
Neste tutorial você aprendeu a criar uma API GraphQL avançada e de alto desempenho com Node.js, Apollo Server e Prisma. Vimos como:
- Estruturar o projeto Node.js e instalar as dependências adequadas.
- Definir o esquema GraphQL usando SDL (types, queries, mutations).
- Implementar resolvers que usam o Prisma Client para fazer operações no banco de dados de forma tipada.
- Configurar o Prisma com o
schema.prisma, migrações e o client para interagir com o banco. - Executar consultas e mutações exemplares, observando como o GraphQL lida com dados relacionados de forma eficiente.
- Otimizar o desempenho, evitando o problema N+1 com DataLoader, habilitando cache e aplicando paginação e limites de profundidade.
Com esses conhecimentos, você está pronto para desenvolver APIs GraphQL completas e escaláveis. Próximos passos podem incluir adicionar recursos como autenticação (JWT, OAuth), subscriptions (WebSockets para atualizações em tempo real) ou até explorar o Apollo Federation para orquestrar micro-serviços GraphQL.
Continue praticando, criando novas consultas customizadas e monitorando sua API em produção. Bons códigos e que sua API seja rápida e confiável!
Quer aprender mais sobre Programação?
Você acabou de ganhar 1 hora com a minha equipe para uma call exclusiva! Vamos entender o seu momento e te mostrar o caminho para se tornar um programador de sucesso. Clique no botão abaixo e agende agora mesmo!