Building Questions: Setting up our backend

ยท

11 min read

Before We Begin

This article aims to walk you through how I would normally set up a brand new Node Js project. Ordinarily, I would have done this with plain old Javascript, but as I mentioned in the Into article for this series, I want to build out the backend for this project with type script, so I'll be learning typescript alongside you guys in this project.

Again if there's anything I don't quite implement correctly or anything I could have implemented more efficiently please let me know in the comments.

So first things first let's set up our new project. P.S. If you are completely new to TypeScript check out this awesome TypeScript crash course by Brad Traversy on youtube.

Setup

The first thing we need to do is to create a new directory that will hold our project, I'm calling mine questions Backend So open up your project directory and run the following commands in your terminal or command prompt

mkdir questionsBackend
cd questionsBackend
npm init -y 
npm i -g typescript
npm install express dotenv
npm i -D typescript @types/express @types/node

Let's look at the commands we've run so far, the first thing we did was create a new directory called questonsBackend. This directory will hold our code for this project. Next, we changed the directory to the questionsBackend directory we just created and set up a new npm project by running npm init -y

The next command we ran npm i -g typescript installed typescript globally on our computer, and after that, we installed express which is a Fast, unopinionated, minimalist web framework for Node.js, and dotenv which is a package that helps read .env files into our project.

TypeScript is a typed version of Javascript and whenever we import 3rd party packages into our application we need to specify what types that 3rd party package works with, what functions and methods it uses, and what kind of arguments they accept and return. Without this information, TypeScript can't check your code. To help TypeScript figure this out we need to add a definition file which helps TypeScript know about the existing properties, functions, arguments, and return values used in the third-party package.

Most definition files are publicly available and placed in the Definitely Typed project on Github. It includes generated types for most of the popular libraries out there. To add a definition file to your project, you need to install an npm module containing @types/{library_name} name. This is what we do with the command npm i -D typescript @types/express @types/node. We are installing the type definitions for express js and node js.

After running the above commands, we are almost done with our project setup, the next thing we need to do is to configure TypeScript within our project. We do this by generating a tsconfig.json file. This file helps us customize TypeScript's behaviour within our project.

Typically, the tsconfig.json file lives at the root of the project. To generate it, weโ€™ll use the tsc command:

npx tsc --init

The command above will generate a new file called tsconfig.json with the following default compiler options:

target: es2016
module: commonjs
strict: true
esModuleInterop: true
skipLibCheck: true
forceConsistentCasingInFileNames: true

You'll probably see a lot of commented-out code in our tsconfig file, but you can safely ignore all of that, we just made two tiny additions.

  "rootDir": "./src", 
   "outDir": "./dist",

The additions above tell typescript where to find our src file and where to output the compiled code. TypeScript is a compiled language and all our type script files will eventually get compiled back into javascript, with the above entries we are telling typescript to look inside our src folder for our TypeScript source files and then when compiling to JavaScript, it should place the converted code in the dist folder in our project directory. Those folders do not exist right now, so we need to create our src folder by running mkdir src and mkdir dist.

Finally, we need to set up some dev tools to make our lives easier, run the following commands

  npm install -D concurrently nodemon

nodemon is a tool that helps NodeJs-based applications by automatically restarting the application whenever file changes in the directory are detected. Concurrently, is another tool which will allow us to run multiple commands at the same time, we will use this to continually re-compile our TypeScript files whenever we make changes and continually restart the compiled Node Js application. To get everything working we update our package.json in the scripts section

 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "npx tsc",
    "start": "node dist/index.js",
    "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\""
  },

Now we are ready to create our very first TypeScript file and set up our express server and database connection, but before we do that let's install some other packages we will need in our project

 npm i bcryptjs mongoose validator cors  jsonwebtoken
 npm i --save-dev @types/validator
 npm i --save-dev @types/bcryptjs 
 npm i --save-dev @types/cors

We installed the following packages :

  1. mongoose: a Node.js-based Object Data Modeling (ODM) library for MongoDB, it pretty much helps makes connecting to MongoDB a lot easier
  2. validator: a library that helps make validating our data a lot easier
  3. cors: a library that helps us deal with Cross-origin resource sharing (Read More)
  4. jsonwebtoken: a library to help with generating JWTs (Read More)
  5. bcryptjs: a library used for hashing passwords.

In our Src folder create a new file called app.ts

import express from "express";
import bodyParser from "body-parser";
import dotenv from "dotenv";
import cors from "cors";

dotenv.config({ path: "./config.env" });

const app = express();
app.use(cors());
app.use(express.json({ limit: "10kb" }));
app.use(bodyParser.urlencoded({ limit: "10kb", extended: false }));

export default app;

In the above code sample, we are importing our libraries and setting up a minimal express server. One important bit to take note of is the line dotenv.config({ path: "./config.env" });. In this line, we are using the dotenv package to load a config file into our application. This config file will hold all our environment variables, so let's create this at the root of our project. the file should be named config.env and should have the following

NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/questions
JWT_SECRET=some-secret-to-the-web-application
JWT_EXPIRES_IN=90d

In our config.env file, we have environment variables specifying what mode our application is currently running on, our database connection URL, our JWT secret which will be used to sign all JWT tokens and the number of days out JWT tokens will last for.

The next step is to create our index.ts file. This file will handle the connection to our database and start-up our express server.

import mongoose, { MongooseError } from "mongoose";
import dotenv from "dotenv";
import app from "./app.js";

dotenv.config({ path: "./config.env" });

const connectionUrl: string = process.env.DATABASE_URL as string;

mongoose
  .connect(connectionUrl)
  .then(() => console.log("DB connection successful"))
  .catch((e: MongooseError) =>
    console.log(`error connecting to database --> ${e}`)
  );

const port = process.env.PORT || 4000;
const mode = process.env.NODE_ENV || "development";
const server = app.listen(port, () => {
  console.log(`App running on ${mode} mode on port ${port}....`);
});

process.on("unhandledRejection", (err: Error) => {
  console.log("unhandled rejection, Shutting down....");
  console.log(err.name, err.message);
  server.close(() => {
    process.exit(1);
  });
});

Let's consider what's happening in the above file. First, we are importing mongoose our MongoDB connection library and the MongooseError type definition, then we import dotenv and finally the application that we configured in our app.ts

Next, we load our environment variables from the config.env file located at the root of our project, this gives us access to all the environment variables we defined in that file. the loaded environment variables can be found accessed via process.env.varaibleName

On the next line, we create a variable which represents our connection URL and then read our database URL from our environment variables and cast it to a string

Next, we use mongoose to connect to our database via the connection URL, the connect method returns a promise, so we attach a then function which will be executed if the promise resolves that is if the connection was successful. If the connection was unsuccessful for any reason, then the catch function will execute. The catch function accepts a parameter e which is of type MongooseError, and it simply logs the error.

Next, we read the port and mode environment variables and if they aren't present, they default to 4000 and development respectively. Then we start our application by listening on the provided port. If the app started successfully, the passed-in function is executed and we log a message.

Finally, we add an event listener to our running application, this listener listens for any unhandled errors and simply closes our server and logs the error message to the console.

Now we can start our application by simply running

  npm run dev

This should start our application in development mode and we should see the following logged to the console.

16:58:52 - Starting compilation in watch mode...
[0] 
[0] 
[0] 16:58:58 - Found 0 errors. Watching for file changes.
[1] App running on development mode on port 4000....
[1] DB connection successful

User Model

We've spent a couple of minutes setting up our application, before we round up this article let's see how we can easily set up our User Model for our application. Firstly, let's create a models directory in our src folder, this folder will hold our data models. After creating the directory, we will create two new files: user.ts and user.interface.ts. Our user.interface.ts will hold the type definition for our user model and it will look like this

export interface IUser {
  firstname: string;
  lastname: string;
  email: string;
  username: string;
  password: string;
  active: boolean;
}

In the above code, we are simply creating an interface which will define what fields our user will have. Our user will have 5 string fields and 1 boolean field, when creating a schema for our user, we will use this Interface to constrain the user schema and specify what fields a user will have.

Next in our user.ts file, we will have the following

import { model, Schema } from "mongoose";
import { IUser } from "./user.interface";
import validator from "validator";
import bcrypt from "bcryptjs";

const userSchema: Schema = new Schema<IUser>({
  email: {
    type: String,
    required: [true, "Email is a required field for user"],
    unique: true,
    lowercase: true,
    validate: [validator.isEmail, "Email provided is not valid"],
  },
  password: {
    type: String,
    required: [true, "please provide a password"],
    minlength: 8,
  },
  username: {
    type: String,
    unique: true,
    required: [true, "username is a required field for user"],
  },
  firstname: {
    type: String,
    required: [true, "firstname is a required field for user"],
  },
  lastname: {
    type: String,
    required: [true, "lastname is a required field for user"],
  },
  active: {
    type: Boolean,
    default: true,
  },
});

userSchema.pre("save", async function (next) {
  if (!this.isModified("password")) return next();
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

const User = model<IUser>("User", userSchema);
export default User;

In the above code, we import schema and model from mongoose, then we import the user interface we defined earlier and then the validator and bcrypt libraries. Note that when defining our user schema we also passed in the user interface we created earlier. const userSchema: Schema = new Schema<IUser>({ ... This is done to properly provide the type definition for our user schema. We also do the same when defining our user model.

In our User schema, we define what fields our user should have. Note that the fields provided match the fields we declared in our user interface, if we add another field to our schema which is not present in our user interface, we would get a compilation error.

After declaring our use schema we go on to define another function

userSchema.pre("save", async function (next) {
  if (!this.isModified("password")) return next();
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

This function is referred to as a pre-save hook, It is a function that will be executed before any user object is saved into our database. In this function, we embed our password hashing logic. In the first line, we check if the password field has been modified or not if the password field has not been modified, we skip the rest of the function by calling the next() function which will move on to any other configured middleware, effectively exiting the function. The reason we check if the password has been modified is to ensure that anytime the password is changed the hashing logic is executed and we hash the password before saving the user to the database. Therefore whenever the user is been created for the first time or whenever the user updates his/her password our pre-save hook will hash the new password before saving the user object.

Finally, we create our user model and export it from our file.

In this article, we've covered how to set up a new typescript express js project, how to connect our express js server to our MongoDB server and how to create a data model.
I don't want to make this article overly long, so we'll stop here, in the next article, we will be setting up authentication for our application and creating our registration and login endpoints. I'll work through the generic way of setting up authentication controllers for our project and then move on to what I feel is a better way of implementing these controllers. If you have any corrections or suggestions on how I can make this article or other articles better, please let me know in the comments. Until next time ๐Ÿ’– & ๐Ÿ’ก

ย