Pular para o conteúdo principal

MongoDB e GraphQL: a combinação perfeita

Aprenda a criar uma API GraphQL com TypeScript e MongoDB — uma combinação não só eficiente, mas também muito fácil de trabalhar.
Atualizado 17 de set. de 2026  · 11 min lido

Explorar com IA

ChatGPTClaudePerplexity

GraphQL é uma forma poderosa e eficiente de construir APIs. O cliente consulta a API como consultaria um banco de dados, e a API retorna apenas os dados solicitados, geralmente reduzindo o payload e melhorando o tempo de resposta. E tempos de resposta baixos são críticos no mundo atual.

Quando a API GraphQL é combinada com MongoDB, além da rapidez, você ganha um formato de dados consistente de ponta a ponta.

Imagine o seguinte: seu cliente executa uma consulta GraphQL que se parece com JSON. Quando esses dados chegam à sua aplicação — digamos que seja em TypeScript — você continua trabalhando com um formato semelhante a JSON na aplicação. Indo além, ao usar MongoDB, os dados enviados e recebidos do banco também são parecidos com JSON. Ou seja, você tem uma experiência de dados consistente com alto desempenho. Menos preocupação em manipular e formatar dados e mais foco na experiência do usuário da aplicação, sem se prender ao banco e às ferramentas.

Neste tutorial, você vai ver como é simples usar MongoDB na sua API GraphQL, desta vez construída com TypeScript.

Pré-requisitos

Para este tutorial, garanta que você tem o seguinte pronto para uso:

  • Um cluster do MongoDB Atlas
  • Node.js 22+
  • MongoDB Node.js Driver 6.x

Não faremos nada muito elaborado no banco neste tutorial, então qualquer tier do cluster MongoDB funciona, inclusive o gratuito. Contudo, não vamos cobrir o provisionamento de um cluster MongoDB Atlas aqui. A expectativa é que você já tenha regras de usuário e de rede configuradas. Se precisar de ajuda para começar, confira a documentação Get Started With Atlas sobre o tema.

Você vai precisar ter uma aplicação Node.js criada. Se preferir, execute os comandos abaixo:

mkdir graphql_example
cd graphql_example
npm init -y

Os comandos acima criam um novo diretório de projeto e um package.json básico. Usaremos TypeScript, Express, MongoDB e Apollo neste projeto. Instale tudo com os comandos a seguir:

npm install @apollo/server@4 body-parser cors dotenv express@4 graphql mongodb
npm install @types/cors @types/express@4 @types/node tsx typescript --save-dev

Eles instalam as dependências e as definições de tipos para uso com TypeScript.

Por fim, crie um arquivo de configuração do TypeScript. Na raiz do projeto, crie o tsconfig.json com o JSON abaixo:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "moduleResolution": "node",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Se em algum momento você quiser ver o projeto completo ou testá-lo, confira no GitHub.

Crie a base com Apollo, Express e dependências do MongoDB

Em termos de estrutura, vamos usar os seguintes arquivos:

  • src/database/connection.ts
  • src/database/userService.ts
  • src/resolvers/index.ts
  • src/schema/typeDefs.ts
  • src/types/index.ts
  • src/index.ts
  • .env

O arquivo src/index.ts terá todo o bootstrap do Express e do Apollo. Ou seja, iniciar o servidor, definir os endpoints GraphQL etc. O arquivo src/types/index.ts conterá as definições TypeScript para trabalhar com o modelo de dados do banco dentro da aplicação. Não confunda com o src/schema/typeDefs.ts, que, apesar de parecido, foca nas definições de tipos do GraphQL.

Isso nos deixa com os diretórios database e resolvers. A maior parte da lógica da aplicação e do banco ficará neles, como veremos já já.

Por agora, vamos configurar o servidor no arquivo src/index.ts:

import 'dotenv/config';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';
import http from 'http';
import cors from 'cors';
import bodyParser from 'body-parser';
import { typeDefs } from './schema/typeDefs';
import { resolvers } from './resolvers';

async function startServer() {
  try {

    const app = express();
    
    const httpServer = http.createServer(app);

    const server = new ApolloServer({
      typeDefs,
      resolvers,
    });

    await server.start();

    app.use(
      '/graphql',
      cors<cors.CorsRequest>(),
      bodyParser.json(),
      expressMiddleware(server)
    );

    const PORT = process.env.PORT || 3000;
    await new Promise<void>((resolve) => httpServer.listen({ port: PORT }, resolve));
    
    console.log(Server ready at http://localhost:${PORT}/graphql);

  } catch (error) {
    console.error('Error starting server:', error);
    process.exit(1);
  }
}

startServer();

Vamos ampliar esse arquivo conforme avançarmos.

Para começar, importamos todas as dependências do projeto. Em seguida, configuramos o servidor Express e o middleware do Apollo.

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

Ainda não adicionamos nada aos arquivos src/schema/typeDefs.ts e src/resolvers/index.ts, mas já deixamos tudo pronto. O servidor não vai subir sem erros até preenchermos essas partes.

Vale mencionar que este exemplo permite requisições de todas as origens por causa do seguinte:

cors<cors.CorsRequest>(),

Isso é bom para desenvolvimento, mas em produção limite às origens necessárias.

Interagindo com o MongoDB a partir da aplicação TypeScript com Node.js

Apesar de já termos o Express e o Apollo configurados, ainda não conectamos o MongoDB na aplicação. Antes de ir ao código, adicione as informações de conexão no arquivo .env:

MONGODB_URI="mongodb+srv://<USERNAME>:<PASSWORD>@<HOST>/?appName=devrel-graphql"
DB_NAME="graphql_example"

Substitua <USERNAME>, <PASSWORD> e <HOST> pelas informações do seu cluster no dashboard do MongoDB Atlas.

Agora, vamos estabelecer a lógica de conexão do banco. No arquivo **src/database/connection.ts**, adicione o código abaixo:

import { MongoClient, Db } from 'mongodb';

let db: Db | null = null;
let client: MongoClient | null = null;

export async function connectToDatabase(): Promise<Db> {
  if (db) {
    return db;
  }

  try {
    const MONGODB_URI = process.env.MONGODB_URI;
    if (!MONGODB_URI) {
      throw new Error('MONGODB_URI is not set');
    }
    const DB_NAME = process.env.DB_NAME || 'graphql_example';
    const APP_NAME = process.env.APP_NAME || 'devrel-graphql';

    console.log('Connecting to MongoDB...');
    
    client = new MongoClient(MONGODB_URI, { appName: APP_NAME });
    await client.connect();
    
    db = client.db(DB_NAME);
    
    console.log(Successfully connected to MongoDB database: ${DB_NAME});
    
    return db;
  } catch (error) {
    console.error('MongoDB connection error:', error);
    throw error;
  }
}

export function getDatabase(): Db {
  if (!db) {
    throw new Error('Database not initialized. Call connectToDatabase() first.');
  }
  return db;
}

export async function closeDatabase(): Promise<void> {
  if (client) {
    await client.close();
    db = null;
    client = null;
    console.log('MongoDB connection closed');
  }
}

A ideia aqui é criar uma conexão singleton. Existem várias formas de usar o MongoDB na sua aplicação, mas escolhemos este caminho para este projeto.

A função connectToDatabase lê as informações do arquivo **.env**, conecta e retorna o banco definido. Se falhar, o erro é capturado e propagado. Você só deve precisar dessa função na configuração inicial do servidor, como vimos no arquivo anterior.

A função getDatabase será usada sempre que uma requisição chegar à aplicação. Ela retorna a instância atual da conexão.

Para manter tudo organizado, temos a função closeDatabase para encerrar a conexão quando o servidor terminar. É opcional, mas uma boa prática.

Antes da lógica de banco, vale definir nossos tipos TypeScript para os DTOs (data transfer objects):

export interface User {
  id: string;
  name: string;
  email: string;
  age?: number;
  createdAt: string;
}

export interface CreateUserInput {
  name: string;
  email: string;
  age?: number;
}

export interface UpdateUserInput {
  id: string;
  name?: string;
  email?: string;
  age?: number;
}

As interfaces acima, no arquivo **src/types/index.ts**, são gerenciadas pela nossa API, mas a lógica de banco também precisa conhecê-las. Você verá já já.

Isso nos leva às interações reais de banco para criar, ler, atualizar e excluir (CRUD), feitas no arquivo src/database/userService.ts. A ideia é: os resolvers do GraphQL tratam a lógica da aplicação e chamam as funções deste serviço, e o serviço cuida da lógica do banco.

No arquivo src/database/userService.ts, temos o seguinte código:

import { Collection, ObjectId } from 'mongodb';
import { getDatabase } from './connection';
import { User, CreateUserInput, UpdateUserInput } from '../types';

interface UserDocument {
  _id: ObjectId;
  name: string;
  email: string;
  age?: number;
  createdAt: Date;
}

export class UserService {
  private collection: Collection<UserDocument>;

  constructor() {
    const db = getDatabase();
    this.collection = db.collection<UserDocument>('users');
  }

  private toUser(doc: UserDocument): User {
    return {
      id: doc._id.toString(),
      name: doc.name,
      email: doc.email,
      age: doc.age,
      createdAt: doc.createdAt.toISOString(),
    };
  }

  async getAllUsers(): Promise<User[]> {
    const users = await this.collection.find().toArray();
    return users.map(doc => this.toUser(doc));
  }

  async getUserById(id: string): Promise<User | null> {
    try {
      const doc = await this.collection.findOne({ _id: new ObjectId(id) });
      return doc ? this.toUser(doc) : null;
    } catch (error) {
      return null;
    }
  }

  async createUser(input: CreateUserInput): Promise<User> {
    const newUser: Omit<UserDocument, '_id'> = {
      name: input.name,
      email: input.email,
      age: input.age,
      createdAt: new Date(),
    };

    const result = await this.collection.insertOne(newUser as UserDocument);
    
    const createdUser = await this.collection.findOne({ _id: result.insertedId });
    
    if (!createdUser) {
      throw new Error('Failed to create user');
    }

    return this.toUser(createdUser);
  }

  async updateUser(input: UpdateUserInput): Promise<User | null> {
    try {
      const updateFields: Partial<Omit<UserDocument, '_id'>> = {};
      
      if (input.name !== undefined) updateFields.name = input.name;
      if (input.email !== undefined) updateFields.email = input.email;
      if (input.age !== undefined) updateFields.age = input.age;

      const result = await this.collection.findOneAndUpdate(
        { _id: new ObjectId(input.id) },
        { $set: updateFields },
        { returnDocument: 'after' }
      );

      return result ? this.toUser(result) : null;
    } catch (error) {
      return null;
    }
  }

  async deleteUser(id: string): Promise<boolean> {
    try {
      const result = await this.collection.deleteOne({ _id: new ObjectId(id) });
      return result.deletedCount > 0;
    } catch (error) {
      return false;
    }
  }

  async createIndexes(): Promise<void> {
    await this.collection.createIndex({ email: 1 }, { unique: true });
    await this.collection.createIndex({ createdAt: -1 });
    console.log('Database indexes created');
  }
}

A primeira coisa a notar é a interface abaixo:

interface UserDocument {
  _id: ObjectId;
  name: string;
  email: string;
  age?: number;
  createdAt: Date;
}

Ela é parecida com a interface User, mas não idêntica. UserDocument modela mais estritamente como o banco trabalha. No banco usamos ObjectId, mas na aplicação usamos strings. No MongoDB, o id é _id, não id, como na interface User.

Isso nos leva à classe UserService.

Depois de obter o handle da coleção a partir da instância do banco no constructor, temos a seguinte série de funções:

  • toUser
  • getAllUsers
  • getUserById
  • createUser
  • updateUser
  • deleteUser
  • createIndexes

A função toUser é um helper para converter entre User e UserDocument. Enviamos e recebemos UserDocument do banco, e precisamos converter para User ao expor pela aplicação.

async createIndexes(): Promise<void> {
	await this.collection.createIndex({ email: 1 }, { unique: true });
	await this.collection.createIndex({ createdAt: -1 });
	console.log('Database indexes created');
}

A função createIndexes cria alguns índices de exemplo que podem ser úteis. Por padrão, existe índice apenas em _id, o que basta para um demo pequeno, mas conforme sua aplicação cresce, não deixe de criar índices adequados aos seus dados e consultas. Os dois acima são apenas exemplos.

Agora podemos olhar cada função do serviço, começando por getAllUsers:

async getAllUsers(): Promise<User[]> {
	const users = await this.collection.find().toArray();
	return users.map(doc => this.toUser(doc));
}

Essa função busca todos os documentos da coleção, pois não há filtro em find. Os resultados são convertidos com toUser e retornados para a aplicação, que serão usados pelos resolvers do Apollo.

async getUserById(id: string): Promise<User | null> {
	try {
	  const doc = await this.collection.findOne({ _id: new ObjectId(id) });
	  return doc ? this.toUser(doc) : null;
	} catch (error) {
	  return null;
	}
}

A função getByUserId é parecida, mas agora usamos findOne com um critério de filtro — neste caso, buscar um único usuário pelo _id, que é único. Como o id em User é string, precisamos envolvê-lo em ObjectId para a consulta no MongoDB.

Agora, criar dados no MongoDB com createUser:

async createUser(input: CreateUserInput): Promise<User> {
	const newUser: Omit<UserDocument, '_id'> = {
	  name: input.name,
	  email: input.email,
	  age: input.age,
	  createdAt: new Date(),
	};
	
	const result = await this.collection.insertOne(newUser as UserDocument);
	
	const createdUser = await this.collection.findOne({ _id: result.insertedId });
	
	if (!createdUser) {
	  throw new Error('Failed to create user');
	}
	
	return this.toUser(createdUser);
}

Após usar insertOne para inserir, usamos o insertedId retornado para consultar o registro. Fazemos isso porque, em uma API GraphQL, ao criar dados, o esperado é já poder consultá-los na mesma mutation.

A função updateUser é um pouco diferente:

async updateUser(input: UpdateUserInput): Promise<User | null> {
	try {
	  const updateFields: Partial<Omit<UserDocument, '_id'>> = {};
	  
	  if (input.name !== undefined) updateFields.name = input.name;
	  if (input.email !== undefined) updateFields.email = input.email;
	  if (input.age !== undefined) updateFields.age = input.age;
	
	  const result = await this.collection.findOneAndUpdate(
		{ _id: new ObjectId(input.id) },
		{ $set: updateFields },
		{ returnDocument: 'after' }
	  );
	
	  return result ? this.toUser(result) : null;
	} catch (error) {
	  return null;
	}
}

Nesta aplicação, permitimos atualização apenas dos campos name, email e age. Todos são opcionais, então só os incluímos no critério de atualização se estiverem presentes.

Isso nos leva ao findOneAndUpdate.

O primeiro objeto é o filtro. Só queremos atualizar quando o _id corresponder. Para cada correspondência, usamos o operador $set para substituir ou criar os campos em updateFields. Campos ausentes não são alterados. Por fim, como estamos encontrando e atualizando, definimos returnDocument como after para receber o documento já alterado. Se usássemos updateOne, teríamos apenas informações da operação, não os dados.

A última operação de CRUD é remover dados do MongoDB com deleteUser:

async deleteUser(id: string): Promise<boolean> {
	try {
	  const result = await this.collection.deleteOne({ _id: new ObjectId(id) });
	  return result.deletedCount > 0;
	} catch (error) {
	  return false;
	}
}

A função deleteOne se assemelha à findOne pois usa o filtro por _id. A diferença é que qualquer correspondência — no máximo um documento — será removida.

Agora a lógica de banco está pronta. Para finalizar, conecte-a a partir de **src/index.ts**:

// Previous imports here...
import { connectToDatabase, closeDatabase } from './database/connection';
import { UserService } from './database/userService';

async function startServer() {
  try {

    await connectToDatabase();
    
    const userService = new UserService();
    await userService.createIndexes();
    
    // The rest of the file here...
}

No trecho acima, omitimos a maior parte do src/index.ts. A ideia é só mostrar o uso de connectToDatabase e a criação dos índices. Em resumo: inclua essas três linhas no topo da função startServer.

A partir daqui, podemos focar na lógica da API.

Defina os tipos GraphQL, resolvers e a lógica da aplicação

Já definimos os tipos TypeScript para trabalhar com o banco e trafegar dados na aplicação. Falta criar as definições de tipos do GraphQL, que determinam o que o usuário pode fazer na API.

No arquivo src/schema/typeDefs.ts, adicione:

export const typeDefs = #graphql
  type User {
    id: ID!
    name: String!
    email: String!
    age: Int
    createdAt: String!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
  }

  type Mutation {
    createUser(name: String!, email: String!, age: Int): User!
    updateUser(id: ID!, name: String, email: String, age: Int): User
    deleteUser(id: ID!): Boolean!
  }
;

O tipo User define o que o cliente pode solicitar em uma consulta. Neste exemplo, ele espelha a interface User, mas não precisa ser assim. Por exemplo, você pode ter um campo como senha que nunca deve ser retornado ao cliente.

O tipo Query define as consultas que podem ser executadas na API. Cada uma retorna um User ou um array de User, e podemos ter lógica específica entre elas.

O tipo Mutation é semelhante ao Query, mas voltado a operações que manipulam dados.

Com as definições de tipos criadas, podemos focar nos resolvers da aplicação. No arquivo src/resolvers/index.ts, inclua o seguinte:

import { UserService } from '../database/userService';
import { User, CreateUserInput, UpdateUserInput } from '../types';

export const resolvers = {
  Query: {
    users: async (): Promise<User[]> => {
      const userService = new UserService();
      return await userService.getAllUsers();
    },

    user: async (_: unknown, { id }: { id: string }): Promise<User | null> => {
      const userService = new UserService();
      return await userService.getUserById(id);
    },
  },

  Mutation: {
    createUser: async (
      _: unknown,
      { name, email, age }: CreateUserInput
    ): Promise<User> => {
      const userService = new UserService();
      return await userService.createUser({ name, email, age });
    },

    updateUser: async (
      _: unknown,
      { id, name, email, age }: UpdateUserInput
    ): Promise<User | null> => {
      const userService = new UserService();
      return await userService.updateUser({ id, name, email, age });
    },

    deleteUser: async (_: unknown, { id }: { id: string }): Promise<boolean> => {
      const userService = new UserService();
      return await userService.deleteUser(id);
    },
  },
};

Os resolvers acima estendem o que vimos nas definições de tipos para queries e mutations.

Veja a query users:

users: async (): Promise<User[]> => {
  const userService = new UserService();
  return await userService.getAllUsers();
},

Essa query usa o UserService para buscar todos os usuários. A resposta User[] é enviada ao cliente, mas apenas com os campos solicitados e que existirem nas definições de tipo GraphQL.

Por exemplo, suponha que o cliente envie esta consulta:

query {
	users {
		name
		email
	}
}

No exemplo acima, temos mais campos disponíveis, mas o cliente quer apenas name e email. Isso é controlado pelas definições de tipo GraphQL com as quais o cliente trabalha. O Apollo faz a mágica acontecer.

As mesmas regras se aplicam a cada query e mutation no arquivo **src/resolvers/index.ts**.

createUser: async (
  _: unknown,
  { name, email, age }: CreateUserInput
): Promise<User> => {
  const userService = new UserService();
  return await userService.createUser({ name, email, age });
},

Na definição de createUser, vimos que o cliente pode enviar name, email e age. No resolver createUser, fazemos o mesmo e chamamos a função createUser do serviço de banco. A resposta volta ao cliente com os campos que ele pediu.

Compile e rode a API GraphQL

A essa altura, a API GraphQL deve estar redonda. Existem algumas formas de compilar e rodar a aplicação.

Por exemplo, você pode adicionar o seguinte ao **package.json**:

"scripts": {
	"build": "tsc",
	"start": "node dist/index.js",
	"dev": "tsx src/index.ts",
	"dev:watch": "tsx watch src/index.ts",
	"watch": "tsc -w"
},

Seguindo esse caminho, rode npm run build para compilar e npm run start para iniciar o servidor. A porta padrão é 3000 e você pode testar com cURL ou a ferramenta de sua preferência.

Conclusão

Você acabou de ver como criar uma API GraphQL com TypeScript e MongoDB como banco. Embora o exemplo seja simples, vimos como construir queries e mutations para realizar operações CRUD no banco. O MongoDB é um par perfeito para APIs GraphQL porque o formato de dados se mantém semelhante do começo ao fim. Por exemplo, o cliente executa uma consulta que se parece com JSON. Os dados que trafegam pela consulta exigem pouca formatação e ajustes — e o mesmo vale na ida e volta ao banco. A experiência é fluida, permitindo focar mais no usuário e menos em manipular dados para satisfazer o banco.

Se você travar em algum ponto, confira o projeto finalizado no GitHub.

FAQs

Why should I use GraphQL?

GraphQL é ótimo porque você consulta uma API como consultaria um banco de dados, pedindo só o que precisa em retorno. Isso torna tudo mais leve e eficiente para o cliente.

Is Apollo the only option for GraphQL?

Apollo facilita muito a criação de APIs GraphQL, mas não é a única opção. No entanto, as outras alternativas fogem ao escopo deste tutorial.

Why use MongoDB with GraphQL?

Ambos trabalham com formatos de dados semelhantes a JSON, então você não precisa ficar convertendo entre estruturas diferentes ao mover dados do banco para a API e para o cliente.

What does a resolver do?

Um resolver é a função que diz ao GraphQL como buscar ou modificar os dados de um campo específico do seu schema.

What's the difference between a query and a mutation?

Queries servem para ler/buscar dados (como requisições GET), enquanto mutations servem para criar, atualizar ou excluir dados (como POST, PUT, DELETE).


Nic Raboy's photo
Author
Nic Raboy

Nic Raboy é Líder de Relações com Desenvolvedores na MongoDB, onde lidera uma equipe de desenvolvedores Python, Java, C# e PHP que criam conteúdo incrível para ajudar os desenvolvedores a serem bem-sucedidos na inclusão do MongoDB em seus projetos. Ele tem experiência com Golang e JavaScript e escreve com frequência sobre muitas de suas aventuras de desenvolvimento.

Tópicos
MongoDB

Principais cursos da DataCamp

Curso

Introdução ao MongoDB em Python

3 h
24.3K
Aprenda a mexer e analisar dados estruturados de forma flexível com o MongoDB.
Ver detalhesRight Arrow
Iniciar Curso
Ver maisRight Arrow
Relacionado

blog

Bancos de dados NoSQL: O que todo cientista de dados precisa saber

Descubra para que servem os bancos de dados NoSQL, por que os cientistas de dados os utilizam e uma lista dos melhores bancos de dados NoSQL disponíveis.
Zoumana Keita 's photo

Zoumana Keita

12 min

blog

Contratos de dados desmistificados: Tudo o que você precisa saber

Obtendo escalabilidade em sistemas de dados distribuídos e reduzindo erros.
Mike Shakhomirov's photo

Mike Shakhomirov

11 min

Tutorial

Como usar GROUP BY e HAVING no SQL

Um guia intuitivo para você descobrir os dois comandos SQL mais populares para agregar linhas do seu conjunto de dados
Eugenia Anello's photo

Eugenia Anello

6 min

Tutorial

Desenvolvimento backend com Python: guia completo para iniciantes

Este guia completo ensina os fundamentos do desenvolvimento backend com Python. Aprenda conceitos básicos, frameworks e boas práticas para começar a criar aplicações web.
Oluseye Jeremiah's photo

Oluseye Jeremiah

15 min

Tutorial

Python JSON Data: Um guia com exemplos

Aprenda a trabalhar com JSON em Python, incluindo serialização, deserialização, formatação, otimização de desempenho, manipulação de APIs e compreensão das limitações e alternativas do JSON.
Moez Ali's photo

Moez Ali

6 min

SQLAlchemy_Tutorial.

Tutorial

Tutorial de SQLAlchemy com exemplos

Aprenda a acessar e executar consultas SQL em todos os tipos de bancos de dados relacionais usando objetos Python.
Abid Ali Awan's photo

Abid Ali Awan

13 min

Ver MaisVer Mais