Menu Docs

Guia: Implementar uma alternativa do Express.js à Atlas Data API

A partir de 2024 de setembro, a Atlas Data API foi descontinuada. Se você usar este serviço, deverá migrar para outra solução antes de 2025 de setembro.

A Atlas Data API permite que os desenvolvedores interajam diretamente com seus clusters MongoDB Atlas usando pontos de conexão HTTP. Este guia demonstra o uso de uma API personalizada baseada em driver para replicar a funcionalidade da Data API.

O modelo da API é criado com Node.js e Express e é compatível com a implantação no Vercel. O serviço de backend é protegido com autorização baseada em chave de API e usa o driver Node.js do MongoDB junto com o Mongoose para executar operações de CRUD (Criar, Ler, Atualizar e Excluir) e de agregação em seus dados.

O código fonte do projeto está disponível no seguinte repositório do GitHub: https://github.com/abhishekmongoDB/data-api-alternative

Importante

Não está pronto para produção

Este modelo não se destina ao uso em um ambiente de produção ou hospedagem de terceiros. Ele tem escopo limitado e demonstra apenas funcionalidade básica. É altamente recomendável implementar aprimoramentos adicionais para atender aos seus requisitos específicos e seguir as práticas recomendadas de segurança.

data-api-alternative/
.
├── index.js
├── connection/
│ └── databaseManager.js
├── controllers/
│ └── dbController.js
├── models/
│ └── dynamicModel.js
├── routes/
│ └── api.js
├── utils/
│ └── logging.js
├── .env
├── package.json
└── vercel.json

Os principais arquivos são:

  • .env: o arquivo de configuração que contém credenciais, detalhes da conexão e configurações do projeto .

  • index.js: o principal ponto de entrada do servidor Express.

  • rotas/api.js: expõe os pontos de conexão da API mapeados para sua lógica de negócios correspondente.

  • connection/databaseManager.js: gerencia e armazena em cache conexões de banco de dados MongoDB .

  • roles/dynamicModel.js: modelos Mongoose gerados dinamicamente que suportam dados Atlas sem esquema.

  • controllers/dbController.js: a lógica de negócios para as operações de CRUD e agregação definidas.

Esses arquivos são descritos em mais detalhes abaixo.

Clone o repositório e instale as dependências:

git clone https://github.com/abhishekmongoDB/data-api-alternative.git
cd data-api-alternative
npm install

Crie um arquivo .env no diretório raiz do projeto e cole o seguinte código:

# Replace with your deployment credentials
MONGO_URI="<MONGO_URI>"
MONGO_OPTIONS="<MONGO_OPTIONS>" # Optional
API_KEY="<API_KEY>"
API_SECRET="<API_SECRET>"
# Project variables
PORT=7438
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes in milliseconds
RATE_LIMIT_MAX=100 # Maximum requests per window
RATE_LIMIT_MESSAGE=Too many requests, please try again later.

Substitua os seguintes placeholders pelas suas credenciais:

  • MONGO_URI Substitua pela string de conexão do seu sistema. Para obter mais informações, consulte Formatos de cadeia de conexão.

    • Instância local: "mongodb://[<user>:<pw>@]localhost"

    • Atlas cluster: "mongodb+srv://[<user>:<pw>@]<cluster>.<projectId>.mongodb.net"

  • MONGO_OPTIONS Substitua pela string de query opcional especificando quaisquer opções específicas da conexão. Para obter mais informações, consulte Opções de connection string.

  • API_KEY Substitua por uma chave de API válida.

  • API_SECRET Substitua pelo segredo de API correspondente.

O servidor Express é inicializado no arquivo index.js. O servidor escuta na porta especificada no arquivo .env.

O servidor inclui middleware para o seguinte:

  • Chave de API e validação secreta usando cabeçalhos x-api-key e x-api-secret.

  • Analisando corpos de solicitação JSON.

  • O roteamento de solicitações para os métodos do controlador com base no endpoint da API.

  • Limitação de taxa. As opções de limite de taxa são especificadas no arquivo .env.

  • Registro usando a biblioteca winston.

index.js
/**
* This file initializes the server, validates API keys for added security,
* and routes requests to the appropriate controller methods.
*/
const rateLimit = require("express-rate-limit");
const express = require("express");
const apiRoutes = require("./routes/api");
const logger = require("./utils/logging");
require("dotenv").config();
const API_KEY = process.env.API_KEY; // Load API key from .env
const API_SECRET = process.env.API_SECRET; // Load API secret from .env
const app = express();
// Middleware for rate limiting
const limiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10), // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX, 10), // Limit each IP to 100 requests per windowMs
message: { message: process.env.RATE_LIMIT_MESSAGE },
});
// Apply the rate limiter to all requests
app.use(limiter);
// Middleware for parsing requests
app.use(express.json());
// Middleware for API key authentication and logging
app.use((req, res, next) => {
logger.info({
method: req.method,
url: req.originalUrl,
body: req.body,
headers: req.headers,
});
const apiKey = req.headers["x-api-key"];
const apiSecret = req.headers["x-api-secret"];
if (apiKey === API_KEY && apiSecret === API_SECRET) {
next(); // Proceed to the next middleware or route
} else {
res.status(403).json({ message: "Forbidden: Invalid API Key or Secret" });
}
});
// Middleware for API routing
app.use("/api", apiRoutes);
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

O arquivo api.js define rotas para operações básicas de CRUD e agregação . As solicitações HTTP POST são mapeadas para suas funções de controlador correspondentes.

rotas/api.js
/**
* Defines the routes for all API endpoints and maps them to the corresponding functions in the dbController class.
*/
const express = require('express');
const dbController = require('../controllers/dbController');
const router = express.Router();
router.post('/insertOne', dbController.insertOne);
router.post('/insertMany', dbController.insertMany);
router.post('/findOne', dbController.findOne);
router.post('/find', dbController.find);
router.post('/updateOne', dbController.updateOne);
router.post('/deleteOne', dbController.deleteOne);
router.post('/deleteMany', dbController.deleteMany);
router.post('/aggregate', dbController.aggregate);
module.exports = router;

O arquivo databaseManager.js lida com as conexões do banco de dados MongoDB usando a biblioteca mongoose. Os detalhes da conexão são armazenados no arquivo .env.

Para obter uma lista exaustiva de opções disponíveis, consulte Opções de conexão na documentação do driver do MongoDB Node.js

connection/databaseManager.js
const mongoose = require("mongoose");
require("dotenv").config();
const connections = {}; // Cache for database connections
/**
* Manages MongoDB database connections.
* @param {string} database - The database name.
* @returns {mongoose.Connection} - Mongoose connection instance.
*/
const getDatabaseConnection = (database) => {
const baseURI = process.env.MONGO_URI;
const options = process.env.MONGO_OPTIONS || "";
if (!baseURI) {
throw new Error("MONGO_URI is not defined in .env file");
}
// If connection does not exist, create it
if (!connections[database]) {
connections[database] = mongoose.createConnection(
`${baseURI}/${database}${options}`
);
// Handle connection errors
connections[database].on("error", (err) => {
console.error(`MongoDB connection error for ${database}:`, err);
});
connections[database].once("open", () => {
console.log(`Connected to MongoDB database: ${database}`);
});
}
return connections[database];
};
module.exports = getDatabaseConnection;

O arquivo de utilitário dynamicModel.js gera modelos Mongoose dinamicamente para o banco de dados e coleção especificados.

Ele também faz o seguinte:

  • Define modelos usando um esquema flexível que pode aceitar quaisquer campos.

  • Modelos em cache para evitar definições de modelo redundantes.

modelos/dynamicModel.js
// Import the mongoose library for MongoDB object modeling
const mongoose = require("mongoose");
// Import the function to get a database connection
const getDatabaseConnection = require("../connection/databaseManager");
// Initialize an empty object to cache models
// This helps in reusing models and avoiding redundant model creation
const modelsCache = {}; // Cache for models (database.collection -> Model)
/**
* Creates and retrieves a dynamic model for a given database and collection.
* This function ensures that the same model is reused if it has already been created.
*
* @param {string} database - The name of the database.
* @param {string} collection - The name of the collection.
* @returns {mongoose.Model} - The Mongoose model instance for the specified collection.
*/
const getModel = (database, collection) => {
// Create a unique key for the model based on the database and collection names
const modelKey = `${database}.${collection}`;
// Check if the model already exists in the cache
// If it does, return the cached model
if (modelsCache[modelKey]) {
return modelsCache[modelKey];
}
// Get the database connection for the specified database
const dbConnection = getDatabaseConnection(database);
// Define a flexible schema with no predefined structure
// This allows the schema to accept any fields
const schema = new mongoose.Schema({}, { strict: false });
// Create the model using the database connection, collection name, and schema
// Cache the model for future use
const model = dbConnection.model(collection, schema, collection);
modelsCache[modelKey] = model;
// Return the newly created model
return model;
};
// Export the getModel function as a module
module.exports = getModel;

O arquivo dbController.js implementa a lógica do controlador para lidar com operações CRUD e agregações. Cada método interage com o MongoDB usando os modelos Mongoose gerados dinamicamente.

Atualmente, a API suporta as seguintes operações:

  • insertOne

  • insertmany

  • encontrar um

  • encontrar muitos

  • UpdateOne

  • UpdateMany

  • Excluir um

  • deleteMany

  • Agregação

controllers/dbController.js
/**
* Contains the logic for all CRUD and aggregation operations.
* Each method interacts with the database and handles errors gracefully.
*/
const getModel = require("../models/dynamicModel");
class DbController {
async insertOne(req, res) {
const { database, collection, document } = req.body;
try {
const Model = getModel(database, collection);
const result = await Model.insertOne(document);
if (!result) {
return res
.status(400)
.json({ success: false, message: "Insertion failed" });
}
res.status(201).json({ success: true, result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}
async insertMany(req, res) {
const { database, collection, documents } = req.body;
try {
const Model = getModel(database, collection);
const result = await Model.insertMany(documents);
if (!result || result.length === 0) {
return res
.status(400)
.json({ success: false, message: "Insertion failed" });
}
res.status(201).json({ success: true, result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}
async findOne(req, res) {
const { database, collection, filter, projection } = req.body;
try {
const Model = getModel(database, collection);
const result = await Model.findOne(filter, projection);
if (!result) {
return res
.status(404)
.json({ success: false, message: "No record found" });
}
res.status(200).json({ success: true, result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}
async find(req, res) {
const { database, collection, filter, projection, sort, limit } = req.body;
try {
const Model = getModel(database, collection);
const result = await Model.find(filter, projection).sort(sort).limit(limit);
if (!result || result.length === 0) {
return res
.status(404)
.json({ success: false, message: "No records found" });
}
res.status(200).json({ success: true, result });
} catch (error) {
r es.status(500).json({ success: false, error: error.message });
}
}
async updateOne(req, res) {
const { database, collection, filter, update, upsert } = req.body;
try {
const Model = getModel(database, collection);
const result = await Model.updateOne(filter, update, { upsert });
if (result.matchedCount === 0) {
return res
.status(404)
.json({ success: false, message: "No records updated" });
}
res.status(200).json({ success: true, result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}
async updateMany(req, res) {
const { database, collection, filter, update } = req.body;
try {
const Model = getModel(database, collection);
const result = await Model.updateMany(filter, update);
if (result.matchedCount === 0) {
return res
.status(404)
.json({ success: false, message: "No records updated" });
}
res.status(200).json({ success: true, result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}
async deleteOne(req, res) {
const { database, collection, filter } = req.body;
try {
const Model = getModel(database, collection);
const result = await Model.deleteOne(filter);
if (result.deletedCount === 0) {
return res
.status(404)
.json({ success: false, message: "No records deleted" });
}
res.status(200).json({ success: true, result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}
async deleteMany(req, res) {
const { database, collection, filter } = req.body;
try {
const Model = getModel(database, collection);
const result = await Model.deleteMany(filter);
if (result.deletedCount === 0) {
return res
.status(404).json({ success: false, message: "No records deleted" });
}
res.status(200).json({ success: true, result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}
async aggregate(req, res) {
const { database, collection, pipeline } = req.body;
try {
const Model = getModel(database, collection);
const result = await Model.aggregate(pipeline);
if (!result || result.length === 0) {
return res
.status(404)
.json({ success: false, message: "No aggregation results found" });
}
res.status(200).json({ success: true, result });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}
}
module.exports = new DbController();

As solicitações de exemplo a seguir demonstram o uso da API com o conjunto de dados de amostra sample_mflix.users. Se você ainda não tiver o conjunto de dados do sample_mflix disponível no seu cluster do Atlas, consulte Carregar dados no Atlas.

Observação

Collection do carteiro disponível

As APIs também estão disponíveis como uma coleção do Postman no repositório do projeto.

Execute o seguinte comando para iniciar o servidor Express. O servidor é recarregado automaticamente toda vez que você salva as alterações em um arquivo de projeto .

npm run dev

Com o servidor em execução, você pode enviar solicitações usando os seguintes comandos cURL.

Antes de executar estes comandos de exemplo , certifique-se de atualizar os espaços reservados <YOUR_API_KEY> e <YOUR_API_SECRET> com suas credenciais.

Os exemplos a seguir demonstram a inserção de um ou vários documentos.

Insira um novo documento de usuário na coleção users:

insertOne
curl -X POST http://localhost:7438/api/insertOne \
-H "Content-Type: application/json" \
-H "x-api-key: <YOUR_API_KEY>" \
-H "x-api-secret: <YOUR_API_SECRET>" \
-d '{
"database": "sample_mflix",
"collection": "users",
"document": {
"name": "Marcus Bell",
"email": "marcus.bell@example.com",
"password": "lucky13" }
}'

Inserir vários documentos de usuário na coleção users:

insertmany
curl -X POST http://localhost:7438/api/insertMany \
-H "Content-Type: application/json" \
-H "x-api-key: <YOUR_API_KEY>" \
-H "x-api-secret: <YOUR_API_SECRET>" \
-d '{
"database": "sample_mflix",
"collection": "users",
"documents": [
{ "name": "Marvin Diaz",
"email": "marvin.diaz@example.com",
"password": "123unicorn" },
{ "name": "Delores Lambert",
"email": "delores.lambert@example.com",
"password": "cats&dogs" },
{ "name": "Gregor Ulrich",
"email": "gregor.ulrich@example.com",
"password": "securePass123" }
]
}'

Os exemplos a seguir demonstram a localização de um ou vários documentos.

Encontre um usuário pelo nome e retorne apenas o endereço de e-mail:

encontrar um
curl -X POST http://localhost:7438/api/findOne \
-H "Content-Type: application/json" \
-H "x-api-key: <YOUR_API_KEY>" \
-H "x-api-secret: <YOUR_API_SECRET>" \
-d '{
"database": "sample_mflix",
"collection": "users",
"filter": { "name": "Marvin Diaz" },
"projection": { "email": 1, "_id": 0 }
}'

Encontre todos os usuários com endereços de e-mail terminando no domínio especificado. Em seguida, classifique os resultados por nome e retorne apenas o nome e o e-mail dos primeiros 10:

encontrar
curl -X POST http://localhost:7438/api/find \
-H "Content-Type: application/json" \
-H "x-api-key: <YOUR_API_KEY>" \
-H "x-api-secret: <YOUR_API_SECRET>" \
-d '{
"database": "sample_mflix",
"collection": "users",
"filter": { "email": { "$regex": "example\\.com$" } },
"projection": { "name": 1, "email": 1, "_id": 0 },
"sort": { "name": 1 },
"limit": 10
}'

Os exemplos a seguir demonstram a atualização de um ou vários documentos.

Atualize o endereço de e-mail de um usuário com o e-mail especificado:

UpdateOne
curl -X POST http://localhost:7438/api/updateOne \
-H "Content-Type: application/json" \
-H "x-api-key: <YOUR_API_KEY>" \
-H "x-api-secret: <YOUR_API_SECRET>" \
-d '{
"database": "sample_mflix",
"collection": "users",
"filter": { "email": "marvin.diaz@example.com" },
"update": { "$set": { "password": "456pegasus" } },
"upsert": false
}'

Atualize o endereço de e-mail de todos os usuários com o domínio especificado:

UpdateMany
curl -X POST http://localhost:7438/api/updateMany \
-H "Content-Type: application/json" \
-H "x-api-key: <YOUR_API_KEY>" \
-H "x-api-secret: <YOUR_API_SECRET>" \
-d '{
"database": "sample_mflix",
"collection": "users",
"filter": { "email": { "$regex": "@example\\.com$" } },
"update": {
"$set": {
"email": {
"$replaceAll": {
"input": "$email",
"find": "@example.com",
"replacement": "@example.org"
}
}
}
},
"upsert": false
}'

Os exemplos a seguir demonstram a exclusão de um ou vários documentos.

Excluir documento com nome especificado:

Excluir um
curl -X POST http://localhost:7438/api/deleteOne \
-H "Content-Type: application/json" \
-H "x-api-key: <YOUR_API_KEY>" \
-H "x-api-secret: <YOUR_API_SECRET>" \
-d '{
"database": "sample_mflix",
"collection": "users",
"filter": { "name": "Delores Lambert" }
}'

Exclua todos os documentos com nomes começando com "M":

deleteMany
curl -X POST http://localhost:7438/api/deleteMany \
-H "Content-Type: application/json" \
-H "x-api-key: <YOUR_API_KEY>" \
-H "x-api-secret: <YOUR_API_SECRET>" \
-d '{
"database": "sample_mflix",
"collection": "users",
"filter": { "name": { "$regex": "^M" } }
}'

O exemplo a seguir demonstra uma operação de agregação que agrupa usuários por e-mail e conta o número de ocorrências:

Agregação
curl -X POST http://localhost:7438/api/aggregate \
-H "Content-Type: application/json" \
-H "x-api-key: <YOUR_API_KEY>" \
-H "x-api-secret: <YOUR_API_SECRET>" \
-d '{
"database": "sample_mflix",
"collection": "users",
"pipeline": [
{ "$group": { "_id": "$email", "count": { "$sum": 1 } } }
]
}'

Este guia exemplificou como você pode implementar uma API personalizada baseada em driver como alternativa à obsoleta Atlas Data API. No entanto, o aplicativo modelo tem escopo limitado e demonstra apenas funcionalidade básica. É altamente recomendável implementar aprimoramentos adicionais para atender aos seus requisitos específicos.

Veja a seguir recursos recomendados para aprimorar o aplicativo modelo:

  • Registro aprimorado: implemente o registro estruturado para melhor observabilidade e depuração.

  • Rastreamento de erros: integre-se a ferramentas de rastreamento de erros para monitorar a integridade da API.

  • Limitação de taxa aprimorada: proteja sua API contra violações com limites de solicitação mais robustos.

  • Segurança: Proteja a API contra vulnerabilidades comuns. Certifique-se de que dados confidenciais e segredos estejam protegidos adequadamente antes de implantá-los, especialmente se estiver hospedando fora das instalações.