Build a MongoDB Document Schema: Examples and Best Practices
We continue from the previous lesson by declaring the shape of the data we expect to store in each of the collections of our database.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two with a single-time purchase.
[00:00 - 00:17] In this lesson, we're going to determine the structure of each document to be stored inside of our collections. If we recall, we've discussed how Mongo and NoSQL databases don't require us to have predefined schema where the data we want inserted in our database.
[00:18 - 00:33] With that being said, our application needs to prepare for the kind of data it expects to receive. As a result, the structure we define here is important since it'll help us prepare for the data we expect in our TypeScript code.
[00:34 - 00:54] First, let's define exactly what a user is in our database, or in other words, the shape of a user document. Although MongoDB automatically creates an underscore ID field of type object ID , we're going to use a string type to represent the ID field of a user document.
[00:55 - 01:18] This is because we're going to use a third-party service to authenticate our users, and that service will instead return a string value to identify a certain user. We'll use that string value as the underscore ID field, and since that value doesn't conform to the object ID type of MongoDB, we'll stick with using the normal string type.
[01:19 - 01:33] We'll get a better understanding of this once we begin the authentication section of our course. A user will have a token field to store the user's login session token, with which will be of type string.
[01:34 - 01:43] A user will have a name field, which is a reference to the user's human readable name. This name field will also be of type string.
[01:44 - 01:54] A user will have an avatar field to store the user's avatar image. This will be of type string, since the data for these fields will be image URLs .
[01:55 - 02:04] We'll have our user to have a contact field of type string. This will be used to store the user's email address.
[02:05 - 02:13] Next, we'll need an identifying field to store our user's payment details. We'll call this field wallet ID.
[02:14 - 02:27] This wallet ID field will be of type string or undefined. The way this field would work is if the user's wallet ID field has a string value, then this user can begin to receive money.
[02:28 - 02:44] If the wallet ID value is undefined, then the user has not linked their payment information, and thus can't receive any money yet. The wallet ID value will be populated once the user has authenticated with our third-party payment processor.
[02:45 - 03:03] Next, we'll have an income field of type number, which will be used to store the user's total income. We'll get a clearer understanding on how the wallet ID and income fields work once we begin the lessons that involve authenticating and using the third-party payment processor.
[03:04 - 03:20] A user document will also have a bookings field to store the user's bookings. This field will be an array of object IDs, and each element in this array refers to a document inside our bookings collection.
[03:21 - 03:32] This type of relationship is a one-to-many relationship. In other words, one user object here will hold references to many different booking objects.
[03:33 - 03:52] A user document will also have a listings field to store the user's listings. This field will also be an array of object IDs, and each element in this array refers to a document inside our listings collection, again a one-to-many relationship.
[03:53 - 04:10] Next, let's define the shape of a listing document in our database. The first thing we need is an underscore ID field that's already defined, and unlike the user document, we'll keep MongoDB's automatically generated type as object ID.
[04:11 - 04:21] Our listings will have a title and description fields both of type string. These fields will be used to store the listings title and description information.
[04:22 - 04:38] We'll state that our listings are to have an image field of type string, which will be used to store the listings image URL. Since each listing must have a host, or in other words, an owner, we'll introduce a host field.
[04:39 - 04:53] This will be used to hold a reference to the host by storing the host user ID. The type for this field will be the same as our user's ID field, so in other words will be of type string.
[04:54 - 05:08] This is a one-to-one relationship. One listing holds one reference to one host. Next, we're interested in introducing a type field, which is to be of one of two values.
[05:09 - 05:20] Either the type is to be an apartment or a house. To define a known set of named constants, we'll use a TypeScript enum type.
[05:21 - 05:37] We'll declare the enum type above our listing interface and call it listing type. Enums in TypeScript are used to define a known set of named constants, and these constants could have numeric values.
[05:38 - 05:53] In our case, we'll want our enum to be a set of members with string values. So we'll state an apartment property of a string value of apartment and a house property of a string value of house.
[05:54 - 06:14] Good convention has us define our enum properties in Pascal case. In our listing interface, we'll set the type of the type field as listing type, which is now set to be one of the two constants we specified in our enum, either the apartment or a house.
[06:15 - 06:30] Now, we'll give our listings an address, country, admin, and city fields, all of which would be of type string. These fields will be used to store the listing's geographic information.
[06:31 - 06:45] Admin is analogous to the concept of states or provinces. We'll get a much clearer understanding of all these fields once we begin to discuss the geocoding of locations in our app.
[06:46 - 07:02] Just like our users, our listings would also need a Bookings field to store any Bookings for itself. This field will be an array of object IDs, each referring to a document inside our Bookings collection.
[07:03 - 07:14] Now, we're going to introduce something a little more complex. How will we ensure when a user books a listing, another user doesn't create a booking where the dates overlap?
[07:15 - 07:30] In software programming, handling dates is hard, incredibly hard in fact. Questions like how do we handle different geographic areas with different time zones, daylight savings time, leap seconds, the ability to compare times, etc.
[07:31 - 07:47] all have to be answered. And many different libraries also exist to help with a lot of these use cases. With that being said though, we're not going to go through a difficult approach to look into how we can best handle how dates are captured in the Bookings of a listing.
[07:48 - 08:08] Instead, we're going to go through a very simple approach and introduce an index that will essentially be nested key value pairs of all the dates that are listing is not available because of a previous booking. As an example, we're going to paste an example of how our index will look.
[08:09 - 08:29] If the following dates are booked, then our Bookings index will look just like this. Our Bookings index is to be nested key value pairs where the first key is a reference to the year a booking is made and then the value are the months in which that booking is to be made.
[08:30 - 08:50] The value in the nested object is a reference to the day of the month in which the booking is to be made which is then given a truly boolean value. Since, for example, a booking is made in 2019 here in the first month and the first and second days, we have the values specified here as true.
[08:51 - 09:16] Note that the JavaScript function for getting the month returns 0 for January and 11 for December. Why are we using objects here as the data structure? This is because values in objects or that is to say hashes can be accessed in constant time, which is much more computationally cheaper than having arrays where we have to iterate through a series of values.
[09:17 - 09:32] Now, this is by all means not a perfect solution, but we'll achieve what we need for our app. If you need the capability to handle dates in your application, we suggest taking your time and finding a good robust solution.
[09:33 - 09:52] In our listing interface, let's introduce a Bookings index field and we'll state the type of this field is Bookings index year. Bookings index year will be an interface to represent the key value pairs of the years a booking is made.
[09:53 - 10:17] Key value pairs in TypeScript can be defined as index signatures, where in this case we'll say the key is to be a string and the value is to be another interface which we'll call Bookings Index Month. The Bookings index month interface would also be key value pairs, but the value in this case would be a Boolean.
[10:18 - 10:30] So here's the representation of the nested object structure of our Bookings index field. It's an object of objects that have values of Booleans.
[10:31 - 10:47] We'll now state our listing documents are to have a price field of type number. And finally, our listing documents will have a number of guests field, which is a number to represent the maximum number of guests a listing can have.
[10:48 - 11:02] And now we'll define exactly what a booking is in our database. A booking is to have the underscore ID field and will keep MongoDB's automatically generated object ID value.
[11:03 - 11:13] We'll want each booking to have a single reference to the listing the bookings being made. So we'll introduce a listing field of type object ID.
[11:14 - 11:26] This is a one-to-one relationship where a booking references the listing is being booked at. We'll also want a booking to have a reference to the tenant of the listing.
[11:27 - 11:36] So we'll introduce a tenant field of type string. Again, this is a one-to-one relationship, but in this case we're stating the value to be of type string.
[11:37 - 11:51] Since the user interface or the user documents ID fields will be of type string . And finally, we'll give our bookings a check-in and checkout fields, which will also be of type string.
[11:52 - 12:00] These fields will be used to store the bookings date information in string format. And that's it.
[12:01 - 12:16] This is pretty much the shape of all the documents we'll have in our collections. With that said, we are going to explain in detail how each of these fields specified in our documents are going to be created and used in our app.
[12:17 - 12:42] Later on in the course, we make two small changes to what we've done in this lesson to sort of resolve a particular bug in one context and improve what we tend to do in the other. So we'll address them step by step. And the first one governs how we actually define the listing type, you know, in this particular case, we define the values for the apartment and house properties as lower case characters.
[12:43 - 12:59] When we create the GraphQL API schema and define the enum type on the GraphQL API, we're going to declare our enum values there in capital letters. And as a result, when we see the data this way, this won't necessarily match with how we're going to define our GraphQL API.
[13:00 - 13:21] So the change we'll make later on to resolve this bug is essentially have the values here declared in capital letters. The other small, I guess, mistake we've made here is how we've defined the type of our bookings index field within the listing interface.
[13:22 - 13:42] Here we've stated that the interface value is bookings index year, which is an object that contains a series of objects, which will then have Boolean values. However, in this context, what we basically specified is that the bookings index field is going to represent only a single object for a single year.
[13:43 - 14:07] But that's not what we want. Instead, what we want is we want to define an interface that we can call bookings index that will essentially contain an index signature where the values in this context is the different bookings index year objects. And essentially, this bookings index interface is the interface we want to define here.
[14:08 - 14:19] So in this context, bookings index now will be an index signature of different objects where the objects represent the years. Each year object will have different objects that represent the different months.
[14:20 - 14:27] And every month object will have different index signatures. And in this context, the values will be Boolean.
[14:28 - 14:36] So these are the two minor changes we're going to make later on. The code samples we share with you are going to have the versions before this.
[14:37 - 14:47] And then where at the certain point in the lecture, when we make the fix or the update, we'll update the code samples from that lesson onwards. However, in your case, you're welcome to follow each as is.
[14:48 - 14:56] You can update it as this before you move forwards. Or you can just have it as what we did in the lesson video and make the update when we get to that point later on.
[14:57 - 15:09] The lesson manuscript for this lesson will reference the way we've done it here , which is the correct way of doing so. However, in the original lesson video and in the code samples we share with you , it will be the prior way of doing things.
[15:10 - 15:15] And we'll make the fix later on and then show the updated code from that point onwards.