PART I: Create a Node.js server with Mongoose and Typescript step by step.

“YOU DON’T HAVE TO BE GREAT TO START, BUT YOU HAVE TO START TO BE GREAT.” –ZIG ZIGLAR

TL;DR

In this post, we are going to present in the first part how to create a node.js server with mongoose and typescript to design a simple application.

In part II, we will focus in the unit and integration tests.

First, we will start by creating users apis which allow to:

  • Create a new user.
  • Update an existing user.
  • Delete a user.
  • Fetch user(s).

Note: I use Node version v12.13.0 and npm v6.13.0 on windows 10.

Let's configure our app using this command:

npm init --yes

If you want to configure manually your package.json and choose the different options, you can just run this command:

npm init

Then, we install the different packages needed to create our server:

$pc>npm i express body-parser mongoose tslint dotenv concurrently --save
$pc>npm i -g typescript tslint 
$pc> npm @types/node @types/express @types/body-parser @types/mongoose

express.js: is a Node.js framework.

body-parser: is a Node.js body parsing middleware.

mongoose: MongoDB Object Data Modeling for Node.js.

dotenv : is a module to load environment variables in an application.

concurrently: is a module that allows to run multiples tasks concurrently.

typescript : is a typed superset of JavaScript that compiles to plain JavaScript.

tslint: is a tool to check TypeScript code for readability, maintainability, and functionality errors.

Note: we installed typescript and tslint packages globally in order to use the different command line instructions to initialize the configuration files.

Then we add some scripts in our package.json:

 "scripts": {
    "start:build": "tsc -w",
    "start:run": "nodemon ./build/server/server.js",
    "start": "set NODE_ENV=development && concurrently npm:start:*"
}

start:build: compile all the .ts files and builds the generated .js files under build directory.

start:run: runs the server.

start: sets the node environment for development and runs the different scripts mentioned above concurrently.

In order to compile our .ts files to .js files, we need to make some configuration to let typescript knows about the root directory of the input files and the output directory of the built files. To do that just run:

tsc --init

a tsconfig.json will be created automatically. In this section we will not concentrate on the different compiler options, but just on the rootDir and outDir options.

Uncomment them, then add the path of the rootDirectory which contains the .ts files and the outputDirectroy's path that will contain the .js files generated by typescript.

Your tsconfig.json should look like this now:

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",  /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs", 
    "outDir": "./build", /* Redirect output structure to the directory. */
    "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
}
}

Then, we generate a configuration file using tslint using:

tslint --init

tslint.json file will be generated automatically and you will get this basic configuration by default:

{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {},
    "rulesDirectory": []
}

After the initialization of the different configuration files, let's create a file containing the environment variables of the application.

Create a .env file under on the root directory with package.json, tsconfig.json and tslint.json.

__MONGO_HOST__=mongodb://localhost:27017
__DEV_DB_NAME__=devDb
__TEST_DB_NAME__=testDb

Then, we create a config.ts under src/server to let dotenv knows about the configuration file path:

import { config } from "dotenv";
import { resolve } from "path";
config({ path: resolve(__dirname, "../../.env") });

To configure the database connection, we create a file connection.ts under src/server/database:

import mongoose from "mongoose";

/* To avoid deprecation warnings mentioned by mongoose when 
starting the database, we set these options according to the 
warning messages.If you want to get more information about 
this issue, visit this link below: 
https://mongoosejs.com/docs/connections.html#options.
*/
const options = {
  useCreateIndex: true,  
  useFindAndModify: false,
  useNewUrlParser: true, 
  useUnifiedTopology: true,
};

let dbName = process.env.__DEV_DB_NAME__;
if (process.env.NODE_ENV === "test") {
  dbName = process.env.__TEST_DB_NAME__;
}
(async () => {
  try {
    await mongoose.connect(`${process.env.__MONGO_HOST__}/${dbName}`, options);
  } catch (error) {
    console.log("Database connection error: ", error);
  }
})();

const db = mongoose.connection;
if (process.env.NODE_ENV !== "test") {
  db.on("error", (error) => {
    console.log("Error while establishing database connection: ", error);
  });

  db.on("open", () => {
    console.log("Connection to database established successfully!!");
  });
}

export default db;

We write an error middleware, that we will use it later in order to catch the errors . So, we create a file called error.ts under src/server/utils:

import { Request, Response } from "express";

export class ErrorHandler extends Error {
  constructor(public statusCode: number, public message: string) {
    super(message);
    this.statusCode = statusCode;
  }
}

export const errorMiddleware = (error: ErrorHandler, req: Request, res: Response) => {
  const statusCode = error.statusCode || 500;
  const message = error.message;

  return res.send(statusCode).send({
    message,
  });
};

Now, we can write the code of our app.ts :

import bodyParser from "body-parser";
import express, { Express } from "express";
import "./config";
import "./database/connection";
import router from "./routes/index";
import { errorMiddleware } from "./utils/error";

const app: Express = express();
app.use(bodyParser.json());
app.use("/", router);
app.use(errorMiddleware);

export { app };

In order to access the apis, we create our index.ts which contains the different routes of the users' application:

import { Router } from "express";
import {
  createUser,
  deleteUser,
  fetchManyUsers,
  fetchOneUser,
  updateUser,
} from "../controllers/index";

const router = Router();

router.route("/users")
  .get(fetchManyUsers)
  .post(createUser);

router.route("/users/:id")
  .delete(deleteUser)
  .get(fetchOneUser)
  .patch(updateUser);

export default router;

To implement the different controllers of the app, we need first of all design the model of the database.

Let's create the User.ts :

import mongoose, { Schema } from "mongoose";

export const UserSchema = new Schema({
  address: {
    type: String,
  },
  email: {
    required: true,
    type: String,
    unique: true,
  },
  name: {
    required: true,
    type: String,
    unique: true,
  },
});

const User = mongoose.model("User", UserSchema);

export default User;

Finally, we create our controllers which are responsible for the different apis mentioned above:

import { Request, Response } from "express";
import User from "../models/User";
import { ErrorHandler } from "../utils/error";

export const createUser = async (req: Request, res: Response) => {
  try {
// check that the req.body is not empty to create a new user
    if ((req.body.constructor === Object && !Object.keys(req.body).length) ||
      (req.body.constructor === Array && !req.body.length)) {
      throw new ErrorHandler(404, "Request body should contain at least one user");
    }
    const createdUser = await User.create(req.body);
    return res.status(201).send(createdUser);
  } catch (error) {
    return res.status(error.statusCode || 500).send(error.message);
  }
};

export const deleteUser = async (req: Request, res: Response) => {
  const { id } = req.params;

  try {
    const fetchedUser = await User.findById(id);
    if (!fetchedUser) {
      throw new ErrorHandler(404, "User not found");
    }
    const deletedUser = await User.findByIdAndRemove({ _id: id });
    return res.status(200).send(deletedUser);
  } catch (error) {
    return res.status(error.statusCode || 500).send(error.message);
  }
};

export const updateUser = async (req: Request, res: Response) => {
  const { id } = req.params;

  try {
    const fetchedUser = await User.findById(id);
    if (!fetchedUser) {
      throw new ErrorHandler(404, "User not found");
    }
    const updatedUser = await User.findByIdAndUpdate({ _id: id }, req.body, {
      new: true,
    });
    return res.status(200).send(updatedUser);
  } catch (error) {
    return res.status(error.statusCode || 500).send(error.message);
  }
};

export const fetchManyUsers = async (req: Request, res: Response) => {
  try {
    const fetchedUser = await User.find({});
    return res.status(200).send(fetchedUser);
  } catch (error) {
    return res.status(error.statusCode || 500).send(error.message);
  }
};

export const fetchOneUser = async (req: Request, res: Response) => {
  const { id } = req.params;
  try {
    const fetchedUser = await User.findById(id);
    if (!fetchedUser) {
      throw new ErrorHandler(404, "User not found");
    }
    return res.status(200).send(fetchedUser);
  } catch (error) {
    return res.status(error.statusCode || 500).send(error.message);
  }
};

We need to separate the app from the server context for the test purposes (we will discuss that in the second part).

We create server.ts which is responsible of the server launch:

import { app } from "./app";

const port = process.env.port || 3000;

app.listen(3000, () => {
  console.log(`Server runs on 127.0.0.1:${port}`);
});

We can now start our server using the script:

npm start

and we get these logs in the terminal:

[start:build] 02:06:33 - Starting compilation in watch mode...
[start:build]
[start:run] [nodemon] 1.18.10
[start:run] [nodemon] to restart at any time, enter `rs`
[start:run] [nodemon] watching: *.*
[start:run] [nodemon] starting `node ./build/server/server.js`
[start:run] Server runs on 127.0.0.1:3000
[start:run] [nodemon] restarting due to changes...
[start:run] [nodemon] restarting due to changes...
[start:run] [nodemon] restarting due to changes...
[start:run] [nodemon] restarting due to changes...
[start:run] [nodemon] restarting due to changes...
[start:build]
[start:build] 02:06:35 - Found 0 errors. Watching for file changes.
[start:run] [nodemon] restarting due to changes...
[start:run] [nodemon] restarting due to changes...
[start:run] [nodemon] restarting due to changes...
[start:run] [nodemon] starting `node ./build/server/server.js`
[start:run] Server runs on 127.0.0.1:3000
[start:run] Connection to database established successfully!!

Summary

In this first part, we tried to describe the different steps in order to design a simple node.js server with typescript and mongoose and set up the different configuration files that we need to launch the server. You will find the complete project on users-demo repository on github. If anyone has any suggestions or find any mistake, or anything to be improved or enhanced in this example, feel free to leave me a comment and if you don't have anything to mention don't forget to like this article.

No Comments Yet