DEV Community

loading...

PokeAPI REST in NodeJS with Express, Typescript, MongoDB and Docker — Part 3

Nya
Full stack dev~
・9 min read

Foreword

This is part 3 of a series of posts which will show you how to create a RESTful API in NodeJS. For further reading please check out the following links:

PokeAPI REST in NodeJS with Express, TypeScript, MongoDB and Docker — Part 1

PokeAPI REST in NodeJS with Express, TypeScript, MongoDB and Docker — Part 2

If you prefer to check out the full code, you can find the full PokeApi project here.

Introduction

In the previous post we deployed an instance of MongoDB with docker-compose, and connected our application to it. We also created our Mongoose Schema and data Model.

In this post we are going to implement the rest of the routes that are necessary to create a basic CRUD, as well as their respective database query functions. These functions will make use of the Model we created previously to query our MongoDB database.

The coding begins

Preview

As always, we will begin with a preview of how our directory will look by the end of this post:

You may note that, directory-wise, nothing has changed since the previous post. Content-wise, however, there are quite a few changes.

Just as a reminder, to run our project we are currently using the following command:

npm run start
Enter fullscreen mode Exit fullscreen mode

To start our dockerized MongoDB instance, use the following command:

docker-compose up
Enter fullscreen mode Exit fullscreen mode

This said, let us begin.

PokeService: Querying the database

It is now time to create our database query functions. For this, as previously mentioned, we are going to make use of our Pokemon Model. Since our goal is to implement the four basic CRUD operations, the first function we are going to implement is one for reading the contents of the db. Open up the pokemon.service.ts file, and type in the following:

//src/services/pokemon.service.ts

import { Request, Response } from 'express';
import { MongooseDocument } from 'mongoose';
import { Pokemon } from '../models/pokemon.model';
import { WELCOME_MESSAGE } from '../constants/pokeApi.constants';

export class PokeService {
  public welcomeMessage(req: Request, res: Response) {
    res.status(200).send(WELCOME_MESSAGE);
  }

//Getting data from the db
  public getAllPokemon(req: Request, res: Response) {
    Pokemon.find({}, (error: Error, pokemon: MongooseDocument) => {
      if (error) {
        res.send(error);
      }
      res.json(pokemon);
    });
  }
 }
Enter fullscreen mode Exit fullscreen mode

As you can see, we’ve created a new function, named “getAllPokemon”. It uses the Pokemon Model to interact with MongoDB and find all the Pokemon in the db.

Since Mongoose’s helper functions are extensively documented in the Mongoose docs, I don’t think it’s necessary to break them down here. I will, however, comment on the guard clause inside the callback:

Note: Guard clauses are, put simply, a check that exits the function, either with a return statement or an exception, in case of an error or fulfilled condition. They allow us to avoid unnecessary complexity (if else if structures) in our code.

This is our guard clause:

if (error) {
   res.send(error);
  }
  res.json(pokemon);
Enter fullscreen mode Exit fullscreen mode

By reversing the logic, and checking first for errors, we can avoid an “else” statement. If any errors are encountered, we exit the function by sending the error. If we find no errors, then the pokemon result is sent. We will make use of this technique throughout the rest of this post.

Implementing GET routing

We now have our getAllPokemon function in our PokeService. To be able to interact with this function, we must create another GET route. Let’s open our controller, and add a new route:

//src/main.controller.ts

import { PokeService } from "./services/pokemon.service";
import { Application } from "express";

export class Controller {
  private pokeService: PokeService;

  constructor(private app: Application) {
    this.pokeService = new PokeService();
    this.routes();
  }

  public routes() {
    this.app.route("/").get(this.pokeService.welcomeMessage);

    //Our new route
    this.app.route("/pokemons").get(this.pokeService.getAllPokemon);
  }
}

Enter fullscreen mode Exit fullscreen mode

As you can see, the endpoint to access this new route is “/pokemons”. (Excuse the glaring grammatical error, it’s to avoid confusion further on.)

From here on, I recommend using Postman to test our routes. You can find more information about Postman here and install it here.

If all goes well, you should be obtaining output like the following from Postman:

Since we haven’t introduced any data into our db, we are receiving an empty array. We have now completed our first db query successfully!

Adding a new Pokemon

Let’s implement a function to add a new pokemon to our db. Go back to the PokemonService and type:

//src/services/pokemon.service.ts

import { Request, Response } from 'express';
import { MongooseDocument } from 'mongoose';
import { Pokemon } from '../models/pokemon.model';
import { WELCOME_MESSAGE } from '../constants/pokeApi.constants';

export class PokeService {
  public welcomeMessage(req: Request, res: Response) {
    res.status(200).send(WELCOME_MESSAGE);
  }

  public getAllPokemon(req: Request, res: Response) {
    Pokemon.find({}, (error: Error, pokemon: MongooseDocument) => {
      if (error) {
        res.send(error);
      }
      res.json(pokemon);
    });
  }

  //Adding a new pokemon

  public addNewPokemon(req: Request, res: Response) {
    const newPokemon = new Pokemon(req.body);
    newPokemon.save((error: Error, pokemon: MongooseDocument) => {
      if (error) {
        res.send(error);
      }
      res.json(pokemon);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

To explain briefly, we create a Mongoose Document (newPokemon) from the request body, and we save it into the db.

Note: Just as a reminder, Mongoose Documents are instances of Mongoose Models

Let’s create the route to interact with our function. In our controller:

//src/main.controller.ts

import { PokeService } from "./services/pokemon.service";
import { Application } from "express";

export class Controller {
  private pokeService: PokeService;

  constructor(private app: Application) {
    this.pokeService = new PokeService();
    this.routes();
  }

  public routes() {
    this.app.route("/").get(this.pokeService.welcomeMessage);
    this.app.route("/pokemons").get(this.pokeService.getAllPokemon);

    //Our new route
    this.app.route("/pokemon").post(this.pokeService.addNewPokemon);
  }
}

Enter fullscreen mode Exit fullscreen mode

Note that our new route is accessed through a POST request. Head over to Postman, and let’s add a new Pokemon to our db:

If everything goes well, you should be receiving the Pokemon you just added as output. To double-check, we can make use of our GET route:

As you can see, we now have a Squirtle in our db. Don’t worry about the “_id” and “__v” fields. They are generated automatically by Mongooose, and we will deal with them later.

Deleting a Pokemon

To implement a function to delete a Pokemon, open up the PokeService, and type the following:

//src/services/pokemon.service.ts

import { Request, Response } from 'express';
import { MongooseDocument } from 'mongoose';
import { Pokemon } from '../models/pokemon.model';
import { WELCOME_MESSAGE } from '../constants/pokeApi.constants';

export class PokeService {
  public welcomeMessage(req: Request, res: Response) {
    res.status(200).send(WELCOME_MESSAGE);
  }

  public getAllPokemon(req: Request, res: Response) {
    Pokemon.find({}, (error: Error, pokemon: MongooseDocument) => {
      if (error) {
        res.send(error);
      }
      res.json(pokemon);
    });
  }

  public addNewPokemon(req: Request, res: Response) {
    const newPokemon = new Pokemon(req.body);
    newPokemon.save((error: Error, pokemon: MongooseDocument) => {
      if (error) {
        res.send(error);
      }
      res.json(pokemon);
    });
  }

  public deletePokemon(req: Request, res: Response) {
    const pokemonID = req.params.id;
    Pokemon.findByIdAndDelete(pokemonID, (error: Error, deleted: any) => {
      if (error) {
        res.send(error);
      }
      const message = deleted ? 'Deleted successfully' : 'Pokemon not found :(';
      res.send(message);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

We obtain the ID of the Pokemon to delete from the request parameters, this is to say, the parameters in the query string in the URL to which we make the GET request. It would look something like this:

localhost:9001/pokemon/123pokemonId
Enter fullscreen mode Exit fullscreen mode

Mongoose has a very useful findByIdAndDelete helper function, which allows us to delete a document (in our case, a Pokemon) by said document’s “_id” field. This function is shorthand for findOneAndDelete({_id: pokemonId}).

Note: If you have the need of deleting a document by any field other than _id, you may use the findOneAndDelete function aforementioned. More information about it here.

I’d now like to draw your attention to the following line:

const message = deleted ? "Deleted successfully" : "Pokemon not found :(";
Enter fullscreen mode Exit fullscreen mode

Here we have a ternary expression, which assigns a different value to the “message” variable, depending on the value of the second parameter (“deleted”) passed to the callback.

This is because Mongoose’s findByIdAndDelete function finds a matching document, deletes it, and then passes the found document (if any) to the callback. Therefore, if Mongoose finds a document, it will be deleted, in which case we return the “Deleted successfully” message. If not, we return the “Pokemon not found” message.

Once we have our function ready, let’s create our route. In our controller:

//src/main.controller.ts

import { PokeService } from "./services/pokemon.service";
import { Application } from "express";

export class Controller {
  private pokeService: PokeService;

  constructor(private app: Application) {
    this.pokeService = new PokeService();
    this.routes();
  }

  public routes() {
    this.app.route("/").get(this.pokeService.welcomeMessage);
    this.app.route("/pokemons").get(this.pokeService.getAllPokemon);
    this.app.route("/pokemon").post(this.pokeService.addNewPokemon);

    //Our new route
    this.app.route("/pokemon/:id").delete(this.pokeService.deletePokemon);
  }
}

Enter fullscreen mode Exit fullscreen mode

In the route we have just created, we are indicating that we will receive a request parameter in the URL, a parameter we have named “id”. This is the parameter we previously used in the Pokemon Service to obtain the id.

Note that this route is accessed through a DELETE request.

Once again, we open Postman, and test our new route by deleting the Squirtle (or whichever Pokemon you chose) we added to our db earlier:

As you can see, we receive the “Deleted successfully” message. If no Pokemon with the id we specified were to be found, we would receive the “Pokemon not found” message instead.

We can double check that our squirtle has been deleted correctly by obtaining all Pokemon from the db:

Empty array = no Pokemon = Squirtle has been deleted successfully.

Updating a Pokemon

In our Pokemon Service:

//src/services/pokemon.service.ts

import { Request, Response } from 'express';
import { MongooseDocument } from 'mongoose';
import { Pokemon } from '../models/pokemon.model';
import { WELCOME_MESSAGE } from '../constants/pokeApi.constants';

export class PokeService {
  public welcomeMessage(req: Request, res: Response) {
    res.status(200).send(WELCOME_MESSAGE);
  }

  public getAllPokemon(req: Request, res: Response) {
    Pokemon.find({}, (error: Error, pokemon: MongooseDocument) => {
      if (error) {
        res.send(error);
      }
      res.json(pokemon);
    });
  }

  public addNewPokemon(req: Request, res: Response) {
    const newPokemon = new Pokemon(req.body);
    newPokemon.save((error: Error, pokemon: MongooseDocument) => {
      if (error) {
        res.send(error);
      }
      res.json(pokemon);
    });
  }

  public deletePokemon(req: Request, res: Response) {
    const pokemonID = req.params.id;
    Pokemon.findByIdAndDelete(pokemonID, (error: Error, deleted: any) => {
      if (error) {
        res.send(error);
      }
      const message = deleted ? 'Deleted successfully' : 'Pokemon not found :(';
      res.send(message);
    });
  }

  //Updating a pokemon

  public updatePokemon(req: Request, res: Response) {
    const pokemonId = req.params.id;
    Pokemon.findByIdAndUpdate(
      pokemonId,
      req.body,
      (error: Error, pokemon: any) => {
        if (error) {
          res.send(error);
        }
        const message = pokemon
          ? 'Updated successfully'
          : 'Pokemon not found :(';
        res.send(message);
      }
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Note that we have used precisely the same technique as in the deletePokemon function. Obtaining the ID as a request parameter, using Mongoose’s findByIdAndUpdate helper function, and returning a message according to the value of the second callback parameter.

In our controller, let’s create the final route:

//src/main.controller.ts

import { PokeService } from "./services/pokemon.service";
import { Application } from "express";

export class Controller {
  private pokeService: PokeService;

  constructor(private app: Application) {
    this.pokeService = new PokeService();
    this.routes();
  }

  public routes() {
    this.app.route("/").get(this.pokeService.welcomeMessage);
    this.app.route("/pokemons").get(this.pokeService.getAllPokemon);
    this.app.route("/pokemon").post(this.pokeService.addNewPokemon);

    //Chaining our route

    this.app
      .route("/pokemon/:id")
      .delete(this.pokeService.deletePokemon)
      .put(this.pokeService.updatePokemon);
  }
}

Enter fullscreen mode Exit fullscreen mode

Considering that both the delete and put routes have exactly the same endpoint, we can chain them as shown above. This way, we don’t have to declare the same route twice, one for each verb.

Let’s head over to Postman, and test our final route. Don’t forget to add a new Pokemon, or you won’t have any data to update! I chose to add another Squirtle, which I will now update:

Let’s obtain all our Pokemon to check on our Squirtle:

Congratulations! Your Squirtle has evolved into a Wartortle, and you have successfully implemented all the basic CRUD functions and their respective routes.

Conclusion

In this post we’ve learnt how to query MongoDB by means of Mongoose Models and Documents. We have also implemented routes to access our CRUD functions.

If you’d like to see the full code for this post, you can do so here (branch “part3” of the pokeAPI project).

Thank you so much for reading, I hope you both enjoyed and found this post useful. Feel free to share with your friends and/or colleagues, and if you have any comments, don’t hesitate to reach out to me! Here’s a link to my twitter page.

Discussion (3)

Collapse
aslasn profile image
Ande

You didn't encounter typescript's annoying type errors when using mongoose models? I'm just having the problem. I created a model and called for let modelRecord = MyModel.create({ data }). Then when i tried to access some data from there like this modelRecord.data.somedata there typerrors like Property not defined in type Document stuff like that

Collapse
mirkorainer profile image
Mirko Rainer

Can we see a more clear example of the code giving you issue?

Collapse
raymondfrontend profile image
raymond-frontend • Edited

Thank you for this tutorial, I needed to understand how to Dockerize my node express app and your explanation was apt.