How to Build Type Safe API with Typegoose and Express.js
Tags: node, javascript, mongodb, tutorial
Published: 2021-12-12 11:54:33

## Overview

One of the most popular libraries for interacting with a MongoDB database is without a doubt mongoose. But we have a problem, if you want to create a totally Type Safe API, you will always have a small loss with mongoose models. I mean, there are ways to get around this, but it would be amazing if you could do this in a totally natural way.

This is where [Typegoose](https://typegoose.github.io/typegoose/) comes in, which is basically a wrapper to create TypeScript templates in an easy way. I bet that at some point you've already tried TypeORM and most likely liked working with classes and decorators to define the fields of your entities.

And Typegoose has a very similar approach and it turns out to be immensely intuitive and after we define our model we end up with two things, the entity itself and the data types.

## Today's example

In today's example I will share with you a simple way to configure a Node.js project with TypeScript and then we will CRUD an entity that we will have in the database.

## Project setup 

As a first step, create a project directory and navigate into it:

```sh
mkdir ts-typegoose
cd ts-typegoose
```

Next, initialize a TypeScript project and add the necessary dependencies:

```sh
npm init -y
npm install typescript ts-node-dev @types/node --save-dev
```

Next, create a tsconfig.json file and add the following configuration to it:

```js
{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext"],
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "esnext",
    "moduleResolution": "Node",
  }
} 
```

Now let's add the following script to our package.json file.

```js
{
  // ...
  "type": "module",
  "scripts": {
    "start": "ts-node-dev main.ts"
  },
  // ...
}
```

Now proceed with the installation of the Express and Typegoose dependencies (as well as their development dependencies):

```sh
npm install @typegoose/typegoose express mongoose --save
npm install @types/express --save-dev
```

# Let's code

And now let's create a simple API:

```js
// @/main.ts
import express, { Request, Response } from "express";

const app = express();

app.use(express.json());

app.get("/", (req: Request, res: Response): Response => {
  return res.json({ message: "Typegoose Example 🤟" });
});

const start = async (): Promise<void> => {
  try {
    app.listen(3000, () => {
      console.log("Server started on port 3000");
    });
  } catch (error) {
    console.error(error);
    process.exit(1);
  }
};

void start();
```

For the API to be initialized on port `3000` just run the following command:

```sh
npm start
```

Now we can import mongoose into our `main.ts` and we will connect to the database.

```js
// @/main.ts
import express, { Request, Response } from "express";
import mongoose from "mongoose";

const app = express();

// ...

const start = async (): Promise<void> => {
  try {
    await mongoose.connect(
      "mongodb://root:root@localhost:27017/typegoose?authSource=admin"
    );
    app.listen(3000, () => {
      console.log("Server started on port 3000");
    });
  } catch (error) {
    console.error(error);
    process.exit(1);
  }
};

void start();
```

Now with the connection made, we can start working on our database entities. In this example we will have a model called *Dog*, which will contain some properties such as name, breed, age and whether our four legged friend has been a good boy or not.

To define our model we will use Typegoose as you might have expected, then we will create a class called Dog that will be public (will be exported). Finally, we will use Typegoose's `getModelForClass()` function which will be responsible for creating the model through the class.

```js
// @/models.ts
import { prop, getModelForClass } from "@typegoose/typegoose";

export class Dog {
  @prop({ required: true })
  public name!: string;

  @prop({ required: true })
  public breed!: string;

  @prop({ required: true })
  public age!: number;

  @prop({ required: false, default: true })
  public isGoodBoy?: boolean;
}

export const DogModel = getModelForClass(Dog);
```

Now we go back to our `main.ts` and we will import the class and our model, the class will be used as data type in our application and the model will be used to interact with the collection in our database.

```js
// @/main.ts
import express, { Request, Response } from "express";
import mongoose from "mongoose";

import { DogModel, Dog } from "./models";

const app = express();

// ...
```

Now that we have everything we need to create our routes, we can start by fetching all the documents we have in our collection.

```js
app.get("/dogs", async (req: Request, res: Response): Promise<Response> => {
  const allDogs: Dog[] = await DogModel.find();
  return res.status(200).json(allDogs);
});
```

Now that we can fetch all documents, now let's fetch only one document from the collection via id.

```js
app.get("/dogs/:id", async (req: Request, res: Response): Promise<Response> => {
  const { id } = req.params;
  const dog: Dog | null = await DogModel.findById(id);
  return res.status(200).json(dog);
});
```

Now we can fetch all documents and only one remains to insert documents into the database. For that, we will create a new document according to the data coming from the request body.

```js
app.post("/dogs", async (req: Request, res: Response): Promise<Response> => {
  const dog: Dog = await DogModel.create({ ...req.body });
  return res.status(201).json(dog);
});
```

Now it's necessary to implement the update of a specific document. Similar to what we did before, we will look for a document in the collection through the id and then we will update the model fields according to the properties coming from the request body.

```js
app.put("/dogs/:id", async (req: Request, res: Response): Promise<Response> => {
  const { id } = req.params;
  await DogModel.updateOne({ id }, req.body);
  const updatedDog: Dog | null = await DogModel.findById(id);
  return res.status(200).json(updatedDog);
});
```

Last but not least, it remains to delete a document from the collection. Again, we will look for this element in the collection through the id and then this same document will be removed.

```js
app.delete("/dogs/:id", async (req: Request, res: Response): Promise<Response> => {
    const { id } = req.params;
    const deletedDog: Dog | null = await DogModel.findOneAndDelete({ id });
    return res.status(200).json(deletedDog);
  }
);
```

As you may have noticed at the endpoints for updating and removing documents from the collection, the data of the updated/deleted element is returned in the response body just so that they have some kind of feedback on the action taken.

The final code of our `main.ts` is as follows:

```js
// @/main.ts
import express, { Request, Response } from "express";
import mongoose from "mongoose";

import { DogModel, Dog } from "./models";

const app = express();

app.use(express.json());

app.get("/dogs", async (req: Request, res: Response): Promise<Response> => {
  const allDogs: Dog[] = await DogModel.find();
  return res.status(200).json(allDogs);
});

app.get("/dogs/:id", async (req: Request, res: Response): Promise<Response> => {
  const { id } = req.params;
  const dog: Dog | null = await DogModel.findById(id);
  return res.status(200).json(dog);
});

app.post("/dogs", async (req: Request, res: Response): Promise<Response> => {
  const dog: Dog = await DogModel.create({ ...req.body });
  return res.status(201).json(dog);
});

app.put("/dogs/:id", async (req: Request, res: Response): Promise<Response> => {
  const { id } = req.params;
  await DogModel.updateOne({ id }, req.body);
  const updatedDog: Dog | null = await DogModel.findById(id);
  return res.status(200).json(updatedDog);
});

app.delete("/dogs/:id", async (req: Request, res: Response): Promise<Response> => {
    const { id } = req.params;
    const deletedDog: Dog | null = await DogModel.findOneAndDelete({ id });
    return res.status(200).json(deletedDog);
  }
);

const start = async (): Promise<void> => {
  try {
    await mongoose.connect(
      "mongodb://root:root@localhost:27017/typegoose?authSource=admin"
    );
    app.listen(3000, () => {
      console.log("Server started on port 3000");
    });
  } catch (error) {
    console.error(error);
    process.exit(1);
  }
};

void start();
```

## Conclusion

As always, I hope you found it interesting. If you noticed any errors in this article, please mention them in the comments. 🧑🏻‍💻

Hope you have a great day! 🧙