Chapter 4: Infrastructure for User. MongoDB database. MongoDB index. Jest testing for TypeScript. Your Settings page. API infrastructure for uploading file.
We regularly update the codebase with stable syntax and stable versions for packages.
The section below is a preview of SaaS Boilerplate Book. To read the full text, you will need to purchase the book.
In Chapter 4, you will start with the codebase in the 4-begin folder of our saas repo and end up with the codebase in the 4-end folder.
We will cover the following topics in this chapter:
Infrastructure for User - User Schema and Model. Type interface. - Static methods and Mongoose methods - Express routes, router and API methods
MongoDB database - MongoDB Atlas - Creating MongoDB document - Connecting database - Testing connection
MongoDB index
Jest testing for TypeScript - Slugify method - Testing slugify method
Your Settings page - Form and input - Data validation
API infrastructure for uploading file - Page's uploadFile - Getting signed request API - Env variables for uploading file - Uploading file using signed request API - Testing file upload - resizeImage
In Chapter 3, you learned how to build an API infrastructure. You learned new concepts such as API methods and Express routes. Here in Chapter 4, we will connect our API project to a database, MongoDB:
https://docs.atlas.mongodb.com/
In Chapter 3, we harcoded a user object inside an Express route at the API server:
server.get('/api/v1/public/get-user', (_, res) => {
res.json({ user: { email: 'team@builderbook.org' } });
});
In Chapter 4, instead of hardcoding a user object, we will actually retrieve data for a user object from the database.
After we connect API to database, the basic data flow (request and response cycles) will look like this:
In addition to connecting a database, you will learn about the testing framework called Jest:
We will discuss when and why you should write tests for your code.
We will create a new page called YourSettings
. This page shows basic information about your account and allows you to edit your name and avatar:
Finally, you will learn about building external, or third-party, API infrastructure. In Chapter 3, you built so-called internal API methods. An internal API method sends a request to our server. An external API method sends a request to a third-party, external server. In this chapter, we will send a request to an AWS S3 server to upload a file (a user's avatar) to that AWS S3 server.
link Infrastructure for User
If we create a MongoDB database and connect it to our API server, we won't be able to read or save data to that database. That's because in order to CRUD (create, read, update, delete) data from MongoDB, we have to learn and implement MongoDB's CRUD API:
https://docs.mongodb.com/manual/crud/
Thus in this section, we will get familiar with MongoDB's CRUD API and apply it to our User Model
. In the next section, we will actually create a new database and connect it to the API server.
Look at the following diagram:
In this section, we will work on the part colored red: Express routes, Static methods, and Mongoose methods.
In the next section, we will work on the part colored black: MongoDB and connection of MongoDB to API server.
link User Schema and Model. Type interface.
As we just mentioned, we need to get familiar with MongoDB's CRUD API. Using native MongoDB's API is straightforward - you would just install a native Node.js driver locally to your machine:
https://docs.mongodb.com/ecosystem/drivers/node/
And follow API documentation:
https://docs.mongodb.com/manual/crud/
However, in this book, we chose not to work with a native Node.js driver for MongoDB. Instead, we decided to show you how to work with a MongoDB abstraction: Mongoose. Abstraction means that Mongoose is built on top of a MongoDB native driver. Although Mongoose has its own API documentation, Mongoose is built on top of MongoDB. This means that whenever you call a Mongoose method, you call a corresponding MongoDB method.
You might ask why you should learn a MongoDB abstraction instead of the native MongoDB driver. A good abstraction library delivers some value on top of the native library. You should choose the native MongoDB driver if your data has no well-defined structure and you plan to write code to validate data types. However, if your data has well-defined structure (shape), then you should go with Mongoose. Mongoose provides you with a so-called Schema and Model in addition to MongoDB's Document.
In this book, we use Mongoose to work with MongoDB. If your data does not have well-defined shape, then you are welcome to learn and use the native MongoDB driver.
MongoDB database stores data as a so-called Document. The format of the Document is BSON. BSON is a binary representation of JSON data:
https://docs.mongodb.com/manual/core/document/
Mongoose Schema allows you to define the shape of the MongoDB Document:
https://mongoosejs.com/docs/guide.html
Simple example of Schema from Mongoose docs:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var blogSchema = new Schema({
title: String, // String is shorthand for {type: String}
author: String,
body: String,
comments: [{ body: String, date: Date }],
date: { type: Date, default: Date.now },
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
});
In Mongoose, Model is a class
:
https://mongoosejs.com/docs/api.html#model_Model
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
Class
is a special type function (built-in). You can assign parameters and methods to the class
. In the below example, height
and width
are parameters (also called properties) and calcArea
is a method:
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
// Method
calcArea() {
return this.height * this.width;
}
}
const square = new Rectangle(10, 10);
console.log(square.height);
console.log(square.calcArea());
On your browser, open Chrome Developer Tools
, navigate to the Console
tab, and paste the above code:
You should see 10
and 100
printed in the browser console.
Besides parameters and methods that you define, class
has special built-in methods - for example, the constructor
method. A constructor
is a special method that creates an object with some initial parameters or initial methods:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor
In the above example, we defined the parameters height
and width
.
In the below example, we defined the parameter name
(also from Mozilla docs):
class Polygon {
constructor() {
this.name = "Polygon";
}
}
const poly1 = new Polygon();
console.log(poly1.name);
// expected output: "Polygon"
You created an object poly1
that has the parameter poly1.name
with a value of "Polygon"
.
You might use the constructor
method if you are familiar with React. In React, if you need to set the initial state or bind some methods to this
of a component or page component, you would use constructor
:
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
In fact, we used the constructor
method when we defined the Notifier
component inside our APP project. Open book/4-start/app/components/common/Notifier.tsx
and find this block:
constructor(props) {
super(props);
openSnackbarExternal = this.openSnackbar;
}
As you can see, we used constructor
to bind (assign) the method openSnackbar
to this
.
So Mongoose Model is a class. How do we create it and extend it?
In Mongoose, we call the model
method to create a subclass of Mongoose Model:
const User = mongoose.model('User', mongoSchema);
In Mongoose, the model
method creates a Model subclass using schema.
An instance of the User
Model is a Mongoose Document, which is also a class. A Mongoose Document represents a one-to-one mapping to documents as stored in MongoDB:
https://mongoosejs.com/docs/documents.html
In the below example, user
is an instance of the User
Model, thus a Document:
const User = mongoose.model('User', mongoSchema);
const user = new User();
Since we are using TypeScript, we have to pass data types - using interface
- to the above definition of User
:
const User = mongoose.model<UserDocument, UserModel>('User', mongoSchema);
UserDocument
and UserModel
are interfaces
:
https://www.typescriptlang.org/docs/handbook/interfaces.html
TypeScript uses interface
to define data structure, parameters (also called properties), and functions (also called methods).
For example:
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
// expected output: "Size 10 Object"
Interface
LabeledValue defines the shape of the object labeledObj
.
When TypeScript compiles code, it will check if labeledObj
, an argument that is passed to the function printLabel
, has the parameter/property label
. TypeScript will also check that the data type of label
is a string
.
In our case, we use two interfaces
: UserDocument
and UserModel
. We use them to define the shape of Document and Model, because when we call the mongoose.model
method, we want to check that all parameters (properties) are present and are the proper data type.
You've reached the end of the Chapter 4 preview. To continue reading, you will need to purchase the book.
We regularly update the codebase with stable syntax and stable versions for packages.