
In our post, we mastered Mutations. We can now query, add, update, and delete films from our Hammer collection. However, every time we restart our Apollo server, our changes vanish into the ether. Our “Watched” list resets, and that film we deleted? It’s back from the dead—and not in a cool, technicolor, cinematic way.
To fix this, we need Data Persistence. In this post, we’ll swap our humble, local JavaScript array for a MongoDB database.
If you haven’t done so already, clone the lab Github repository using
git clone https://github.com/jmwollny/lab.gitInstall MongoDB
I’m installing on a Mac, if you what to install MongoDB on other systems go to the MongoDB download page here.
brew tap mongodb/brew
brew install mongodb-communityNow start the MongoDB server. This command ensures that the MongoDB server will restart at logon.
brew services start mongodb/brew/mongodb-communityNow check we have a running instance. Type mongosh. If the shell appears you are golden and are ready to proceed to the next section. Type exit to leave the shell.
Setting Up the MongoDB connection
First, we need to install the MongoDB driver. In your terminal, run:
cd lab/graphql-tutorial-3
npm install mongooseMongoose is an Object Data Modeling (ODM) library that makes talking to MongoDB from Node.js much easier. If you open index.js you will see that the films array has been replaced with a MongoDB connection to a database called hammer_films.
const mongoose = require('mongoose');
// Connect to your local or Atlas MongoDB instance
mongoose.connect('mongodb://localhost:27017/hammer_films', {
useNewUrlParser: true,
useUnifiedTopology: true
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => console.log('Connected to MongoDB!'));
Defining the Data Model
In GraphQL, we have a Schema. In MongoDB (via Mongoose), we have a Model. These two need to mirror each other so our data flows correctly. A new file called Film.js contains the MongoDB model which has been exported so it can be shared by seed.js(more about this later!).
const mongoose = require('mongoose');
const filmSchema = new mongoose.Schema({
title: { type: String, required: true },
year: { type: Number, required: true },
watched: { type: Boolean, default: false }
});
// Export the model so both index.js and seed.js can use it
module.exports = mongoose.model('Film', filmSchema);Updating the Resolvers
This is where the magic happens. Instead of using .find() or .splice() on a local array, we will use Mongoose methods which return Promises. GraphQL handles these asynchronous calls automatically.
The New Queries and mutations
const resolvers = {
Query: {
films: async (parent, args) => {
// 1. Build a dynamic query object
let query = {};
// Watch filter
if (args.watched !== undefined) {
query.watched = args.watched;
}
// Year filter (Exact match)
if (args.year) {
query.year = args.year;
}
// Date range filter (using MongoDB operators $gte and $lte)
if (args.where) {
query.year = query.year || {}; // Initialize year object if it doesn't exist
if (args.where.year_gte) {
query.year.$gte = args.where.year_gte;
}
if (args.where.year_lte) {
query.year.$lte = args.where.year_lte;
}
}
// Search filter (using Regex for case-insensitive partial match)
if (args.searchTerm) {
query.title = { $regex: args.searchTerm, $options: 'i' };
}
// Execute the query against the database
return await FilmModel.find(query);
},
// Find by ID - Mongoose maps GraphQL 'id' to MongoDB '_id' automatically
film: async (parent, args) => await FilmModel.findById(args.id),
},
Mutation: {
addFilm: async (parent, { input }) => {
// Create a new instance and save it
const newFilm = new FilmModel(input);
return await newFilm.save();
},
updateWatched: async (parent, { id, watched }) => {
const updatedFilm = await FilmModel.findByIdAndUpdate(
id,
{ watched },
{ new: true }, // This flag returns the record *after* it was updated
);
if (!updatedFilm) {
throw new Error('Film not found');
}
return updatedFilm;
},
deleteFilm: async (parent, { id }) => {
const deleted = await FilmModel.findByIdAndDelete(id);
if (!deleted) {
throw new Error('Film not found');
}
return await FilmModel.find();
},
},
};Testing Persistence
Restart your server with node index.js. Now, head back to your GraphQL sandbox at http://localhost:4000/. We can test that after adding a film and restarting our Apollo server, the film still exists!
In the sandbox run a mutation to add a new film.
mutation CreateFilm($input: CreateFilmInput!) {
addFilm(input: $input) {
id
title
year
watched
}
}Remember to add the variables JSON.
{
"input": {
"title": "The Brides of Dracula",
"year": 1960,
"watched": false
}
}Run the query then shut down your server (Ctrl + C in the terminal). Start the server again usig node index.js
Run a query to retrieve all films.
query GetAllFilms {
films {
id
title
watched
year
}
}Query result
{
"data": {
"films": [
{
"id": "69dfb8e8067cfb4bcaadeb6d",
"title": "The Brides of Dracula",
"watched": false,
"year": 1960
}
]
}
}If all has gone well, your data is still there! Unlike our local array, MongoDB has written this data to the disk.
Why use Mongoose with GraphQL?
You might notice that our FilmModel and our GraphQL type Film look very similar. This redundancy is actually a strength. The GraphQL Schema acts as a contract for your frontend (telling it what data it can ask for), while the Mongoose Model acts as a gatekeeper for your database (telling it how the data must be stored).
The “ID” Gotcha
MongoDB uses a field called _id by default. GraphQL usually expects id. Mongoose is smart enough to provide a virtual id field that maps to _id, so the existing queries like film(id: "...") continue to work without a hitch.
Importing the full list of films
Let’s finish by importing our film list into MongoDB, then we can get down to the fun job of watching every one and marking them as watched as we go.
To do this I have provided a handy script. Running the script will clear the database and import all films. All you need to do is open a terminal and run node seed.js.
node seed.js
Connected to MongoDB for seeding...
Old records removed.
157 Hammer films successfully added to the database!Let’s have a look at the script.
const mongoose = require('mongoose');
const fs = require('fs');
// Import your Mongoose model
const Film = require('./models/Film');
const seedDatabase = async () => {
try {
// Connect to MongoDB
await mongoose.connect('mongodb://127.0.0.1:27017/hammer_films');
console.log("Connected to MongoDB for seeding...");
// Read the JSON file
const data = JSON.parse(fs.readFileSync('./films.json', 'utf-8'));
// Clear existing films
await Film.deleteMany({});
console.log("Old records removed.");
// Bulk insert the data
await Film.insertMany(data);
console.log(`${data.length} Hammer films successfully added to the database!`);
// Close the connection
process.exit();
} catch (error) {
console.error("Error seeding database:", error);
process.exit(1);
}
};
seedDatabase();This script does the following:
- Connects to MongoDB
- Parses the list of films(note: we do not need the id anymore in the JSON)
- Deletes all records in the database
- Inserts all records defined in the JSON file
Conclusion
We’ve successfully moved our Hammer database from a “temporary” state to a “permanent” one. By integrating MongoDB, we’ve laid the groundwork for a real-world application. We are no longer just playing with variables in memory. we are managing a persistent data store. As an exercise try creating different queries or if you are feeling brave add more fields to the schema. Have fun coding and if you feel inclined watching one of the suggested films 🙂