DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for How to Build Type Safe API with Typegoose and Express.js
Francisco Mendes
Francisco Mendes

Posted on

How to Build Type Safe API with Typegoose and Express.js

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 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:

mkdir ts-typegoose
cd ts-typegoose
Enter fullscreen mode Exit fullscreen mode

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

npm init -y
npm install typescript ts-node-dev @types/node --save-dev
Enter fullscreen mode Exit fullscreen mode

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

{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext"],
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "esnext",
    "moduleResolution": "Node",
  }
} 
Enter fullscreen mode Exit fullscreen mode

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

{
  // ...
  "type": "module",
  "scripts": {
    "start": "ts-node-dev main.ts"
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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

npm install @typegoose/typegoose express mongoose --save
npm install @types/express --save-dev
Enter fullscreen mode Exit fullscreen mode

Let's code

And now let's create a simple API:

// @/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();
Enter fullscreen mode Exit fullscreen mode

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

npm start
Enter fullscreen mode Exit fullscreen mode

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

// @/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();
Enter fullscreen mode Exit fullscreen mode

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.

// @/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);
Enter fullscreen mode Exit fullscreen mode

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.

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

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

const app = express();

// ...
Enter fullscreen mode Exit fullscreen mode

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

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

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

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

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);
  }
);
Enter fullscreen mode Exit fullscreen mode

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:

// @/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();
Enter fullscreen mode Exit fullscreen mode

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! πŸ§™

Top comments (2)

Collapse
 
suhakim profile image
sadiul hakim

@typegoose is awesome

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

That's true 😁

Classic DEV Post from 2020:

js visualized

πŸš€βš™οΈ JavaScript Visualized: the JavaScript Engine

As JavaScript devs, we usually don't have to deal with compilers ourselves. However, it's definitely good to know the basics of the JavaScript engine and see how it handles our human-friendly JS code, and turns it into something machines understand! πŸ₯³

Happy coding!