Voltar ao blog

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.

  1. Crie uma nova pasta para o projeto e entre nela:

    mkdir minha-api-graphql cd minha-api-graphql
  2. Inicialize um projeto Node.js e responda às perguntas do npm init. Para pular, pode usar -y:

    npm init -y
  3. Instale as dependências principais: o Apollo Server, GraphQL e o Prisma. No terminal, execute:

    npm install apollo-server graphql prisma @prisma/client

    Isso 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.
  4. 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 init

    Isso criará uma pasta prisma/ com o arquivo schema.prisma e um arquivo .env. Você pode configurar o banco ajustando schema.prisma ou o .env. Para seguir este tutorial, aceitaremos o SQLite padrão (o arquivo SQLite será criado em dev.db). O conteúdo inicial do schema.prisma será parecido com:

    datasource db { provider = "sqlite" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" }
  5. (Opcional) Caso queira usar outro banco como PostgreSQL, edite o provider e a url em schema.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 User e Post, 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 Query e Mutation, usamos métodos do prisma para consultar o banco. Por exemplo, prisma.user.findMany() retorna todos usuários.
  • Em User.posts e Post.author, criamos resolvers de campo para buscar relacionamentos entre modelos.
  • Note que convertemos id para número (Number(id)), pois no schema do Prisma id é do tipo Int.

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 model no 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 campo posts: Post[] em User e referenciando authorId em Post.
  • author User @relation(...) em Post e o campo authorId definem 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 com id = 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 production por 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=production e 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!