Creating a User Profile Store for a Game With Node.js and MongoDB
Karen Huaulme, Nic RaboyPublished Jan 12, 2022 • Updated Feb 03, 2023
Rate this tutorial
When it comes to game development, or at least game development that has an online component to it, you're going to stumble into the territory of user profile stores. These are essentially records for each of your players and these records contain everything from account information to what they've accomplished in the game.
Take the game Plummeting People that some of us at MongoDB (Karen Huaulme, Adrienne Tacke, and Nic Raboy) are building, streaming, and writing about. The idea behind this game, as described in a previous article, is to create a Fall Guys: Ultimate Knockout tribute game with our own spin on it.
Since this game will be an online multiplayer game, each player needs to retain game-play information such as how many times they've won, what costumes they've unlocked, etc. This information would exist inside a user profile document.
In this tutorial, we're going to see how to design a user profile store and then build a backend component using Node.js and MongoDB Realm for interacting with it.
To get you up to speed, Fall Guys: Ultimate Knockout is a battle royale style game where you compete for first place in several obstacle courses. As you play the game, you get karma points, crowns, and costumes to make the game more interesting.
Since we're working on a tribute game and not a straight up clone, we determined our Plummeting People game should have the following data stored for each player:
- Experience points (XP)
- Steps taken
- Collisions with players or objects
- Pineapples (Currency)
- Inventory (Outfits)
- Plummie Tag (Username)
Of course, there could be much more information or much less information stored per player in any given game. In all honesty, the things we think we should store may evolve as we progress further in the development of the game. However, this is a good starting point.
Now that we have a general idea of what we want to store, it makes sense to convert these items into an appropriate data model for a document within MongoDB.
Take the following, for example:
Notice that we have the information previously identified. However, the structure is a bit different. In addition, you'll notice extra fields such as
created_atand other timestamp-related data that could be helpful behind the scenes.
For achievements, an array of objects might be a good idea because the achievements might change over time, and each player will likely receive more than one during the lifetime of their gaming experience. Likewise, the
inventoryfield is an object with arrays of objects because, while the current plan is to have an inventory of player outfits, that could later evolve into consumable items to be used within the game, or anything else that might expand beyond outfits.
One thing to note about the above user profile document model is that we're trying to store everything about the player in a single document. We're not trying to maintain relationships to other documents unless absolutely necessary. The document for any given player is like a log of their lifetime experience with the game. It can very easily evolve over time due to the flexible nature of having a JSON document model in a NoSQL database like MongoDB.
To get more insight into the design process of our user profile store documents, check out the on-demand Twitch recording we created.
With a general idea of how we chose to model our player document, we could start developing the backend responsible for doing the create, read, update, and delete (CRUD) spectrum of operations against our database.
Since Express.js is a common, if not the most common, way to work with Node.js API development, it made sense to start there. What comes next will reproduce what we did during the Twitch stream.
From the command line, execute the following commands in a new directory:
The above commands will initialize a new package.json file within the current working directory and then install Express.js, the MongoDB Node.js driver, and the Body Parser middleware for accepting JSON payloads.
Within the same directory as the package.json file, create a main.js file with the following Node.js code:
There's quite a bit happening in the above code. Let's break it down!
You'll first notice the following few lines:
We had previously downloaded the project dependencies, but now we are importing them for use in the project. Once imported, we're initializing Express and are telling it to use the body parser for JSON and URL encoded payloads coming in with POST, PUT, and similar requests. These requests are common when it comes to creating or modifying data.
Next, you'll notice the following lines:
clientin this example assumes that your MongoDB Atlas connection string exists in your environment variables. To be clear, the connection string would look something like this:
Yes, you could hard-code that value, but because the connection string will contain your username and password, it makes sense to use an environment variable or configuration file for security reasons.
collectionvariable is being defined because it will have our collection handle for use within each of our endpoint functions.
Speaking of endpoint functions, we're going to skip those for a moment. Instead, let's look at serving our API:
In the above code we are serving our API on port 3000. When the server starts, we establish a connection to our MongoDB Atlas cluster. Once connected, we make use of the
plummeting-peopledatabase and the
plummiescollection. In this circumstance, we're calling each player a plummie, hence the name of our user profile store collection. Neither the database or collection need to exist prior to starting the application.
Time to focus on those endpoint functions.
To create a player — or plummie, in this case — we need to take a look at the POST endpoint:
The above endpoint expects a JSON payload. Ideally, it should match the data model that we had defined earlier in the tutorial, but we're not doing any data validation, so anything at this point would work. With the JSON payload an
insertOneoperation is done and that payload is turned into a user profile. The result of the create is sent back to the user.
If you want to handle the validation of data, check out database level schema validation or using a client facing validation library like Joi.
With the user profile document created, you may need to fetch it at some point. To do this, take a look at the GET endpoint:
In the above example, all documents in the collection are returned because there is no filter specified. The above endpoint is useful if you want to find all user profiles, maybe for reporting purposes. If you want to find a specific document, you might do something like this:
The above endpoint takes a
plummie_tag, which we're expecting to be a unique value. As long as the value exists on the
plummie_tagfield for a document, the profile will be returned.
Even though there isn't a game to play yet, we know that we're going to need to update these player profiles. Maybe the
xpincreased, or new
achievementswere gained. Whatever the reason, a PUT request is necessary and it might look like this:
In the above request, we are expecting a
plummie_tagto be passed to represent the document we want to update. We are also expecting a payload to be sent with the data we want to update. Like with the
updateOneis experiencing no prior validation. Using the
plummie_tagwe can filter for a document to change and then we can use the
$setoperator with a selection of changes to make.
The above endpoint will update any field that was passed in the payload. If the field doesn't exist, it will be created.
One might argue that user profiles can only be created or changed, but never removed. It is up to you whether or not the profile should have an
activefield or just remove it when requested. For our game, documents will never be deleted, but if you wanted to, you could do the following:
The above code will take a
plummie_tagfrom the game and delete any documents that match it in the filter.
It should be reiterated that these endpoints are expected to be called from within the game. So when you're playing the game and you create your player, it should be stored through the API.
While Node.js with Express.js might be popular, it isn't the only way to build a user profile store API. In fact, it might not even be the easiest way to get the job done.
During the Twitch stream, we demonstrated how to offload the management of Express and Node.js to Realm.
As part of the MongoDB data platform, Realm offers many things Plummeting People can take advantage of as we build out this game, including triggers, functions, authentication, data synchronization, and static hosting. We very quickly showed how to re-create these APIs through Realm's HTTP Service from right inside of the Atlas UI.
To create our GET, POST, and DELETE endpoints, we first had to create a Realm application. Return to your Atlas UI and click Realm at the top. Then click the green Start a New Realm App button.
We named our Realm application PlummetingPeople and linked to the Atlas cluster holding the player data. All other default settings are fine:
Congrats! Realm Application Creation Achievment Unlocked! 👏
Now click the 3rd Party Services menu on the left and then Add a Service. Select the HTTP service. We named ours RealmOfPlummies:
Click the green Add a Service button, and you'll be directed to Add Incoming Webhook.
Let's re-create our GET endpoint first. Once in the Settings tab, name your first webhook getPlummies. Enable Respond with Result set the HTTP Method to GET. To make things simple, let's just run the webhook as the System and skip validation with No Additional Authorization. Make sure to click the Review and Deploy button at the top along the way.
In this service function editor, replace the example code with the following:
In the above code, note that MongoDB Realm interacts with our
plummiescollection through the global
contextvariable. In the service function, we use that context variable to access all of our
plummies.We can also add a filter to find a specific document or documents, just as we did in the Express + Node.js endpoint above.
Switch to the Settings tab of
getPlummies, and you'll notice a Webhook URL has been generated.
We can test this endpoint out by executing it in our browser. However, if you have tools like Postman installed, feel free to try that as well. Click the COPY button and paste the URL into your browser.
If you receive an output showing your plummies, you have successfully created an API endpoint in Realm! Very cool. 💪😎
Now, let's step through that process again to create an endpoint to add new plummies to our game. In the same RealmOfPlummies service, add another incoming webhook. Name it
addPlummieand set it as a POST. Switch to the function editor and replace the example code with the following:
If you go back to Settings and grab the Webhook URL, you can now use this to POST new plummies to our Atlas plummeting-people database.
And finally, the last two endpoints to
Name a new incoming webhook
removePlummieand set as a POST. The following code will remove the
plummiefrom our user profile store:
The final new incoming webhook
updatePlummieand set as a PUT:
With that, we have another option to handle all four endpoints allowing complete CRUD operations to our
plummiedata - without needing to spin-up and manage a Node.js and Express backend.
You just saw some examples of how to design and create a user profile store for your next game. The user profile store used in this tutorial is an active part of a game that some of us at MongoDB (Karen Huaulme, Adrienne Tacke, and Nic Raboy) are building. It is up to you whether or not you want develop your own backend using the MongoDB Node.js driver or take advantage of MongoDB Realm with webhook functions.
This particular tutorial is part of a series around developing a Fall Guys: Ultimate Knockout tribute game using Unity and MongoDB.