Guia: Implementar uma alternativa do Express.js à Atlas Data API
Nesta página
- Estrutura do projeto
- Começar
- Pré-requisitos
- Configurar o projeto
- Definir variáveis de ambiente
- Inicialize o servidor
- Definir as rotas
- Conecte-se ao MongoDB
- Gerar modelos dinamicamente
- Implemente o controlador
- Exemplo de solicitações de API
- Iniciar o Servidor
- Enviar solicitações
- Próximos passos
- Recursos adicionais
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.
Estrutura do projeto
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.
Começar
Pré-requisitos
Um cluster MongoDB implementado ou uma instância local com uma string de conexão válida.
Uma chave de API do Atlas válida para autenticar solicitações. Você pode conceder acesso de programação no nível da organização ou do projeto.
A última versão estável do Node.js e npm (Node Package Manager) instalados.
Configurar o projeto
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
Definir variáveis de ambiente
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.
Inicialize o servidor
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
ex-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
.
/** * 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}`); });
Definir as rotas
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.
/** * 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;
Conecte-se ao MongoDB
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
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;
Gerar modelos dinamicamente
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.
// 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;
Implemente o controlador
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
/** * 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();
Exemplo de solicitações de API
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.
Iniciar o Servidor
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
Enviar solicitações
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.
Insira documentos
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
:
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
:
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" } ] }'
Encontrar documentos
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:
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:
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 }'
Atualize documentos
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:
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:
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 }'
Excluir
Os exemplos a seguir demonstram a exclusão de um ou vários documentos.
Excluir documento com nome especificado:
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":
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" } } }'
Documentos agregados
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:
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 } } } ] }'
Próximos passos
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.
Recursos adicionais
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.