Express Typescript Mongodb Project From Scratch
Express Typescript Mongodb Project From Scratch

In this post you will learn how to implement Mongodb Typescript connection in the real Express + NodeJS application. So what we want to implement here is a typical API inside NodeJS with Express (obviously covered with Typescript) and Mongodb for our database.

The project

And here I already prepared for us an empty project. It just has an empty package.json and a Typescript config.

{
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "target": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "strict": true
  },
  "lib": ["es2015"]
}

Here is a standard Typescript config for our NodeJS application. This is why our module is set to commonjs and our moduleResolution to node. Also it is important that we are using here strict mode which means our Typescript will be as strict as possible.

Installing dependencies

Our first step is to install nodemon.

This is a package which allows us to restart our application with every single change.

npm i nodemon -D

We install it only as a development dependency because we don't need it in production.

"scripts": {
  "start": "nodemon main.ts"
}

Here we added a script to call our main file with nodemon inside our package.json.

This code won't work because we try to execute a Typescript file. nodemon can transpile Typescript files but we need to install an additional dependency for this.

npm i ts-node -D

Here we installed ts-node as a dev dependency so nodemon can transpile it.

Our next step here is to install Express. We need express because it's a framework that we want to use in order to build our API.

npm i express

And additionally to that we want to install types for Express so Typescript can help us.

npm i @types/express -D

Now we can create a basic project in our main.ts.

import express from 'express'

const app = express()

app.listen(3012, () => {
  console.log('API is started')
})

Now when we call npm start our application is started and our API is available for us.

Installing MongoDB

Now we want to implement Typescript MongoDB connection. For this we want to install a MongoDB package but there are lots of different libraries for this. We will use a native driver which is quite low level but it will show you how to use MongoDB correctly.

But in order to start working with MongoDB you must install MongoDB package on your machine and start MongoDB.

I already have a running MongoDB so now I need to install a dependency of MongoDB

npm i mongodb

Also I want to mention here that we don't need types as they are included together with the package.

Now I want to created an additional file db.js with several methods which will help us to work with the database.

import { Db, MongoClient } from "mongodb";

const state: { db: Db | null } = { db: null };

export const connect = async (url: string, dbname: string): Promise<void> => {
  try {
    if (state.db) {
      return;
    }

    const client = new MongoClient(url);

    await client.connect();

    state.db = client.db(dbname);
  } catch (err) {
    console.error(err);
  }
};

export const get = (): Db => {
  if (!state.db) {
    throw new Error("Connection is not initialized");
  }

  return state.db;
};

Here we have 3 different things. First of all an object state where we will store a reference to our database. We also create a connect method which creates a MongoClient connection and saved a reference to state.db. Also we have a get method which returns us a reference to our database so we can get it from any place of our application.

Our database helper is fully ready. Now we need to connect to database before we even start our application.

const startServer = async () => {
  await connect("mongodb://localhost:27017/api", "app");

  app.listen(3012, () => {
    console.log("API is started");
  });
};

startServer();

With such code we are waiting for successful MongoDB connection before we start our API.

Creating a type

What I want to do now is create models and controllers. What does it mean? For our API we want to define functions which respond with some data. These are our controllers. Our models is something which is responsible for working with the database.

Models is just a bunch of helpers to work with database more effeciently.

Before we start with controllers let's create an additional type for our entity.

export type Artist = {
  _id: string
  name: string
}

Additionally to that we need a type for not saved Artist.

export type ArtistWithoutId = Omit<Artist, "_id">;

Here we just omitted an _id for unsaved record.

Creating a model

Let's start with creating a model which is responsible for managing artists in the database.

// src/models/artists.ts
import { Artist } from "../types/artist";
import * as db from "../db";

export const all = (): Promise<Artist[]> => {
  return db.get().collection("artists").find<Artist>({}).toArray();
};

We have here a all method which gets a list of artists from the database.

Any database request is an asynchronous request so it will return a promise.

Creating controller

First of all let's create a controller for entity artists. Inside we will write all methods which are responsible for artists API.

import * as Artists from "../models/artists";
import {
  NextFunction,
  Request as ExpressRequest,
  Response as ExpressResponse,
} from "express";

export const all = async (
  _: ExpressRequest,
  res: ExpressResponse,
  next: NextFunction
) => {
  try {
    const docs = await Artists.all();
    res.send(docs);
  } catch (err) {
    next();
  }
};

Our all method inside controller is responsible for responding to our API call. Inside it uses model so we don't work with database directly in the controller. Inside Express all our route callbacks get req, res and next as parameters

The last step is to register our route in the application.

import * as artistsController from "./controllers/artists";

const app = express();

app.get("/artists", artistsController.all);

All

As you can see in Postman we got an empty array as a response which is totally fine because we don't have any records on our database.

Creating an artist

Now we want to implement creation of our artist. But in order to do that we must install one more package to parse a body of the request.

npm i body-parser

Now we must configure it correctly.

import bodyParser from "body-parser";

const app = express();

app.use(bodyParser.json());
...

Now we will get body inside our request.

In order to implement create artist we must do exactly the same stuff as with all. It will be a method in our model and a method in our controller.

// src/models/artist.ts
export const create = async (artist: ArtistWithoutId): Promise<Artist> => {
  await db.get().collection("artists").insertOne(artist);
  return artist as Artist;
};

Here we use insertOne to create our new artist in the database. Keep in mind that insertOne doesn't return us a created record but changes our artist property instead.

// src/controllers/artist.ts
export const create = async (
  req: ExpressRequest,
  res: ExpressResponse,
  next: NextFunction
) => {
  try {
    const doc = await Artists.create({ name: req.body.name });
    res.send(doc);
  } catch (err) {
    next();
  }
};

Our create method in controller is extremely similar to all method. We call our model and send the response to our client.

// src/main.ts
app.post("/artists", artistsController.create);

And last but not least is we register our new route.

Create

As you can see in browser our artist was successfully created.

Finding an artist

Similar code we need to implement getting of the artist by ID.

// src/models/artists.ts
export const findById = (id: string): Promise<Artist | null> => {
  return db
    .get()
    .collection("artists")
    .findOne<Artist>({ _id: new ObjectId(id) });
};

We find here an artist by ID. Most importantly we must wrap our id with new ObjectId because this is how MongoDB stores IDs.

// src/controllers/artist.ts
export const findById = async (
  req: ExpressRequest,
  res: ExpressResponse,
  next: NextFunction
) => {
  try {
    const doc = await Artists.findById(req.params.id);
    res.send(doc);
  } catch (err) {
    next();
  }
};

Getting an artist by ID is exactly like previous methods in controller.

// src/main.ts
app.get("/artists/:id", artistsController.findById);

And here we registered our new route.

Find

As you can see in browser we can find our artists by ID now.

Updating an artist

Another method what we must implement is updating our artist.

// src/models/artists.ts
export const update = async (
  id: string,
  newData: ArtistWithoutId
): Promise<Artist> => {
  await db
    .get()
    .collection("artists")
    .updateOne({ _id: new ObjectId(id) }, { $set: newData });

  return {
    ...newData,
    _id: id,
  };
};

In our create we pass the ID that we want to update and an object with new data. This method doesn't return the update artist so we merge it and return ourselves.

// src/controllers/artist.ts
export const update = async (
  req: ExpressRequest,
  res: ExpressResponse,
  next: NextFunction
) => {
  try {
    const doc = await Artists.update(req.params.id, { name: req.body.name });
    res.send(doc);
  } catch (err) {
    next();
  }
};

Our controller method is extremely similar to previous methods.

// src/main.ts
app.put("/artists/:id", artistsController.update);

Here is our new route to update the user.

Update

As you can see now we can update our artist by ID.

Deleting an artist

The last route that we need to implement is deleting an artist by ID.

// src/models/artists.ts
export const deleteById = (id: string): Promise<DeleteResult> => {
  return db
    .get()
    .collection("artists")
    .deleteOne({ _id: new ObjectId(id) });
};

We call deleteOne and again wrap our id with new ObjectId in order to work with MongoDB.

// src/controllers/artist.ts
export const deleteById = async (
  req: ExpressRequest,
  res: ExpressResponse,
  next: NextFunction
) => {
  try {
    await Artists.deleteById(req.params.id);
    res.sendStatus(200);
  } catch (err) {
    next();
  }
};

Our controller just calls a model and returns 200 status if there was no error.

// src/main.ts
app.delete("/artists/:id", artistsController.deleteById);

The last step here is to register our new route for deletion.

As you can see in Postman we can remove our create artist by ID now.

And actually if you want to improve your Typescript knowledge and prepare for the interview I highly recommend you to check my course Typescript Interview Questions.

📚 Source code of what we've done