How to Use MongoDB Atlas: Free Cloud Database Setup (2026)
Set up a free MongoDB Atlas cloud cluster, connect with Mongoose, and build a full CRUD app. Plus a real comparison of Atlas free vs Supabase, Neon, and PlanetScale free tiers.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
Getting a database running for a side project should take 10 minutes, not 10 hours. MongoDB Atlas is the closest thing to "zero friction" database hosting I've found for getting something off the ground quickly. Sign up, click a few buttons, copy a connection string, and you're connected.
This guide walks through the Atlas free tier setup from scratch, connects it to a Node.js app with Mongoose, builds a complete CRUD API, and ends with an honest comparison of the major free-tier cloud database options. Whether you're building your first API or testing a new idea, this covers what you actually need to know.
What MongoDB Atlas Is (and What It Isn't)
MongoDB Atlas is the managed cloud version of MongoDB. Instead of installing MongoDB on a server, configuring replication, managing backups, and worrying about uptime, Atlas handles all of that. You get a connection string and focus on writing application code.
The M0 free tier is a shared cluster — your data lives on hardware shared with other Atlas users, with limited resources. For anything under moderate traffic, that's completely fine. According to MongoDB's documentation, M0 supports up to 500 connections, 512MB storage, and gives you access to the full MongoDB 7.0 feature set (except a few enterprise features).
What Atlas is not: it's not a magic solution to schema design problems. MongoDB's document model is flexible — maybe too flexible without discipline. We'll cover basic schema design with Mongoose to add structure.
Step 1: Create Your Atlas Account and Cluster
Go to cloud.mongodb.com and create a free account.
After email verification:
- Click "Build a Database"
- Choose M0 Free (shared cluster)
- Select your cloud provider and region — pick one geographically close to your users (or to your app server if you're hosting elsewhere)
- Give your cluster a name (e.g.,
my-app-cluster) - Click "Create Cluster" — takes about 1-3 minutes to provision
While the cluster provisions, set up access:
Create a database user:
- Go to Database Access → Add New Database User
- Choose Password authentication
- Username:
appuser(or whatever you want) - Password: generate a strong one, save it somewhere
- Database User Privileges: "Read and Write to any database" for development
Whitelist your IP:
- Go to Network Access → Add IP Address
- For development: "Add Current IP Address"
- For production: add your server's specific IP
- For testing from anywhere:
0.0.0.0/0(allows all IPs — fine for testing, not recommended for production)
Step 2: Get Your Connection String
Once the cluster is ready:
- Click "Connect" on your cluster
- Choose "Connect your application"
- Driver: Node.js, version 5.5 or later
- Copy the connection string — looks like:
mongodb+srv://appuser:<password>@my-app-cluster.abc12.mongodb.net/?retryWrites=true&w=majority&appName=my-app-cluster
Replace <password> with your actual database user password. Add your database name before the ?:
mongodb+srv://appuser:yourpassword@my-app-cluster.abc12.mongodb.net/myappdb?retryWrites=true&w=majority
Store this in your .env file, never directly in code:
# .env
MONGODB_URI=mongodb+srv://appuser:yourpassword@my-app-cluster.abc12.mongodb.net/myappdb?retryWrites=true&w=majority
PORT=3000
Step 3: Connect with Mongoose
Mongoose adds schema validation, type casting, and a clean query API on top of the MongoDB Node.js driver. For most applications it's the right choice over using the driver directly.
npm install mongoose dotenv
Create a connection module:
// src/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
// These options are the defaults in Mongoose 7+, but explicit is good
serverSelectionTimeoutMS: 5000, // Timeout after 5 seconds if can't connect
socketTimeoutMS: 45000, // Close socket after 45 seconds of inactivity
});
console.log(`MongoDB connected: ${conn.connection.host}`);
} catch (err) {
console.error('MongoDB connection error:', err.message);
process.exit(1); // Exit on connection failure — don't run without DB
}
};
// Handle connection events
mongoose.connection.on('disconnected', () => {
console.log('MongoDB disconnected');
});
mongoose.connection.on('error', (err) => {
console.error('MongoDB error:', err);
});
module.exports = connectDB;
In your main app file:
// src/index.js
require('dotenv').config();
const express = require('express');
const connectDB = require('./db');
const app = express();
app.use(express.json());
// Connect to MongoDB before starting the server
connectDB().then(() => {
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
});
Step 4: Define Mongoose Schemas and Models
Mongoose schemas define the shape and validation rules for your documents. This is where you add the structure that MongoDB's native format doesn't enforce:
// src/models/Post.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
const PostSchema = new Schema({
title: {
type: String,
required: [true, 'Title is required'],
trim: true,
minlength: [3, 'Title must be at least 3 characters'],
maxlength: [500, 'Title cannot exceed 500 characters']
},
content: {
type: String,
required: [true, 'Content is required'],
minlength: [10, 'Content must be at least 10 characters']
},
excerpt: {
type: String,
maxlength: 300,
default: function() {
// Auto-generate excerpt from content if not provided
return this.content.substring(0, 150) + '...';
}
},
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
tags: {
type: [String],
default: []
},
published: {
type: Boolean,
default: false
},
views: {
type: Number,
default: 0,
min: 0
},
metadata: {
type: Schema.Types.Mixed,
default: {}
}
}, {
timestamps: true, // Adds createdAt and updatedAt automatically
versionKey: false // Remove the __v field from documents
});
// Index for common query patterns
PostSchema.index({ author: 1, published: 1 });
PostSchema.index({ tags: 1 });
PostSchema.index({ createdAt: -1 });
PostSchema.index({ title: 'text', content: 'text' }); // Text search index
// Instance method
PostSchema.methods.incrementViews = function() {
this.views += 1;
return this.save();
};
// Static method
PostSchema.statics.findPublished = function(limit = 20) {
return this.find({ published: true })
.sort({ createdAt: -1 })
.limit(limit)
.populate('author', 'name email');
};
const Post = mongoose.model('Post', PostSchema);
module.exports = Post;
// src/models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
}, { timestamps: true, versionKey: false });
module.exports = mongoose.model('User', UserSchema);
Step 5: Build the CRUD API
// src/routes/posts.js
const express = require('express');
const router = express.Router();
const Post = require('../models/Post');
// GET /posts — list published posts with pagination
router.get('/', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = Math.min(parseInt(req.query.limit) || 10, 100);
const skip = (page - 1) * limit;
const [posts, total] = await Promise.all([
Post.find({ published: true })
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.populate('author', 'name')
.select('-content'), // Exclude full content from list view
Post.countDocuments({ published: true })
]);
res.json({
data: posts,
meta: { page, limit, total, pages: Math.ceil(total / limit) }
});
} catch (err) {
res.status(500).json({ error: 'Failed to fetch posts', message: err.message });
}
});
// GET /posts/:id — single post
router.get('/:id', async (req, res) => {
try {
const post = await Post.findById(req.params.id)
.populate('author', 'name email');
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
// Increment view count asynchronously (don't await — don't block the response)
post.incrementViews().catch(console.error);
res.json(post);
} catch (err) {
// Handle invalid ObjectId format
if (err.name === 'CastError') {
return res.status(400).json({ error: 'Invalid post ID format' });
}
res.status(500).json({ error: 'Failed to fetch post' });
}
});
// POST /posts — create new post
router.post('/', async (req, res) => {
try {
const post = new Post({
...req.body,
author: req.user?.id // Assuming auth middleware sets req.user
});
const saved = await post.save();
res.status(201).json(saved);
} catch (err) {
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(e => ({
field: e.path,
message: e.message
}));
return res.status(400).json({ error: 'Validation failed', details: errors });
}
res.status(500).json({ error: 'Failed to create post' });
}
});
// PATCH /posts/:id — update post
router.patch('/:id', async (req, res) => {
try {
// Use findByIdAndUpdate with runValidators to validate changes
const post = await Post.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true, runValidators: true } // new: true returns updated doc
);
if (!post) return res.status(404).json({ error: 'Post not found' });
res.json(post);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// DELETE /posts/:id — delete post
router.delete('/:id', async (req, res) => {
try {
const post = await Post.findByIdAndDelete(req.params.id);
if (!post) return res.status(404).json({ error: 'Post not found' });
res.status(204).send();
} catch (err) {
res.status(500).json({ error: 'Failed to delete post' });
}
});
// GET /posts/search?q=keyword — text search
router.get('/search', async (req, res) => {
try {
const query = req.query.q;
if (!query) return res.status(400).json({ error: 'Search query required' });
const posts = await Post.find(
{ $text: { $search: query }, published: true },
{ score: { $meta: 'textScore' } }
)
.sort({ score: { $meta: 'textScore' } })
.limit(20);
res.json(posts);
} catch (err) {
res.status(500).json({ error: 'Search failed' });
}
});
module.exports = router;
Atlas Free Tier Limits and What They Mean
| Limit | M0 Free | Notes |
|---|---|---|
| Storage | 512 MB | Shared, no dedicated allocation |
| RAM | Shared | No guarantee; varies with cluster load |
| vCPU | Shared | Throttled under heavy load |
| Connections | 500 | Shared across all connections |
| Collections | 500 | Per project |
| Backups | None | No automated backups on M0 |
| Change Streams | No | Requires M10+ |
| Atlas Search | Limited | Basic text index available |
| Regions | 1 | No multi-region on M0 |
| Upgrade path | M10 ($57/mo) | First paid tier |
500MB is enough for: a personal blog (thousands of posts), a todo/task app for hundreds of users, a portfolio site, an API prototype, or a small e-commerce catalog. It's not enough for: image/file storage (use S3 for that, store URLs in Atlas), high-volume event logs, or apps with millions of active users.
Free Tier Comparison: Atlas vs Supabase vs PlanetScale vs Neon
| Feature | MongoDB Atlas M0 | Supabase Free | PlanetScale Free | Neon Free |
|---|---|---|---|---|
| Database type | MongoDB (NoSQL) | PostgreSQL | MySQL (serverless) | PostgreSQL (serverless) |
| Storage | 512 MB | 500 MB | 5 GB | 0.5 GB (scale on demand) |
| Connections | 500 (shared) | 60 | Unlimited (serverless) | Unlimited (pooled) |
| Compute | Shared | Shared | Serverless (scales to 0) | Serverless (scales to 0) |
| Backups | None | 7 days | None | None |
| Branching | No | No | Yes (database branching) | Yes (database branching) |
| Expires? | Never | Never | Pauses after 7 days inactive | Never |
| Paid tier | M10 $57/mo | Pro $25/mo | Scaler $39/mo | Launch $19/mo |
| Best for | Document data, JS/Node apps | PostgreSQL, full-stack with auth | MySQL users, branching workflows | PostgreSQL, serverless apps |
The PlanetScale caveat: their free tier pauses after 7 days of inactivity and was previously removed entirely — check current status before relying on it for anything persistent. Neon's serverless model means it scales to zero when not in use, which is great for cost but adds cold start latency on first connection after idle time.
For relational database comparisons, PostgreSQL vs MySQL covers the deeper trade-offs. If you're considering a PostgreSQL-backed setup, Prisma ORM PostgreSQL shows the ORM workflow that parallels Mongoose here.
For the API layer on top of your Atlas database — structuring your routes, authentication, file uploads — see API tutorial for beginners, API authentication guide, and Node.js file upload Multer.
If you want to understand the SQL equivalent of the queries we built, SQL basics cheatsheet provides a quick reference for comparison.
Conclusion
MongoDB Atlas gives you a real, production-grade database with zero setup cost and zero expiration. The free M0 tier is legitimate for small production apps, side projects, and learning — not just a trial account that disappears in 30 days.
The workflow we covered — create cluster, get connection string, define Mongoose schemas with validation and indexes, build CRUD routes — is the same workflow you'd use for a full production app. The only difference at scale is switching from M0 to M10 and adjusting your connection pool size.
Start with connectDB(), define one schema for your primary resource, build the five CRUD routes, and you have a working API in under an hour. That's the point.
Go create your cluster — it takes about 3 minutes and costs nothing.
Frequently Asked Questions
Does MongoDB Atlas free tier expire?
No. The MongoDB Atlas M0 (free) tier does not expire — it's genuinely always-free, not a trial. You get a shared cluster with 512MB of storage, shared RAM and CPU, and connection limits. The main restrictions are no dedicated resources, limited to one free cluster per project, no backups, and no advanced features like change streams or performance advisor. It's real production use for small projects, not just a trial.
What happens when I exceed the 512MB storage limit on Atlas free tier?
MongoDB Atlas will send you email warnings as you approach the limit, typically at 75%, 90%, and 100%. Once you exceed 512MB, you won't lose data — Atlas blocks new writes until you either delete data to get under the limit or upgrade to a paid tier (M10+). You won't be auto-billed. The free tier never converts to a paid plan without your explicit action. For most beginner projects and small production apps, 512MB is sufficient — a typical document-based blog or todo app can store hundreds of thousands of records in that space.
Should I use MongoDB Atlas or a relational database like PostgreSQL for my project?
It depends on your data shape. MongoDB Atlas shines when your data is document-shaped, varies in structure between records, or needs flexible schema evolution (adding new fields without migrations). It's particularly strong for content management, user profiles, product catalogs, and event logging. PostgreSQL is better when data is highly relational (many tables with complex JOIN requirements), when you need ACID transactions across multiple collections, or when your team thinks naturally in SQL. For beginners: if you're building a REST API with Node.js and your data is JSON-like, MongoDB Atlas is a fast, friction-free starting point.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
AWS vs Azure vs GCP for Startups: Pricing and Free Tier Guide 2026
AWS, Azure, or GCP for your startup in 2026? Real free tier limits, monthly cost estimates, and honest recommendations based on your actual use case.
Docker for Beginners: Learn Containers in 1 Hour (2026)
Learn Docker from scratch in 2026. Understand containers vs images, write your first Dockerfile, and master essential commands in under an hour.
Build a REST API With Node.js + Express + MongoDB (Full Tutorial)
Step-by-step tutorial to build a production-ready CRUD REST API using Node.js, Express, and MongoDB with models, routes, controllers, and error handling.
Git and GitHub Complete Guide for Beginners — 2026 Edition
Master Git and GitHub from scratch — init, commit, branches, merging, pull requests and team workflows. The only guide you need for version control in 2026.