Curso
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 é 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.

