Brian Love
Angular + TypeScript Developer in Denver, CO

TypeScript: Declaring Mongoose Schema + Model

Reading time ~3 minutes

So, you’re on the TypeScript bandwagon and you want to use Mongodb and mongoose. Great! So, now you need to define your schema and model. So, what is a good TypeScript developer to do? Well, first we turn to npm to install the dependencies and declaration files. This took me awhile to get right, so I hope it saves you some time.

Getting Started

We’ll start by using npm to install mongoose and the TypeScript declaration files using the new @types definitions with TypeScript 2.

$ npm install mongoose --save
$ npm install @types/mongoose --save-dev
$ npm install @types/mongodb --save-dev

Interface

First, let’s create a public interface for our model. In this example I am going to be using a: User. I created a new file at src/interfaces/user.ts:

export interface IUser {
  email?: string;
  firstName?: string;
  lastName?: string;
}

This simply represents a user, and will be helpful when populating our mongodb collections.

Schema + Model

For the schema and model, I put both of these into a new file at src/schemas/user.ts:

import { Document, Schema, Model, model} from "mongoose";
import { IUser } from "../interfaces/user";

export interface IUserModel extends IUser, Document {
 fullName(): string;
}

export var UserSchema: Schema = new Schema({
 createdAt: Date,
 email: String,
 firstName: String,
 lastName: String
});
UserSchema.pre("save", next => {
 let now = new Date();
 if (!this.createdAt) {
 this.createdAt = now;
 }
 next();
});
UserSchema.methods.fullName = function(): string {
  return (this.firstName.trim() + " " + this.lastName.trim());
};

export const User: Model<IUserModel> = model<IUserModel>("User", UserSchema);

Let’s dig into what is going on here:

  • First, I am importing the Document, Schema and Model classes, as well as the model() function.
  • I am also importing our IUser interface that we created previously.
  • Next, I create the IUserModel. Note that it extends both the IUser interface as well as the Document class.
  • Next, I create the Schema. Unfortunately, you have to list all of the schema properties again. This means that whenever you modify the schema, you will need to modify the interface as well, and vice-versa.
  • Next, I define a pre-save hook to populate the created_at date.
  • Next, I am defining a instance method. In this case I have a simple fullname() method that returns the first name concatenated with the user’s last name.
  • Lastly, using the UserSchema we create a new model using mongoose’s model() function.

In Action

Ok, now let’s see this in action. I have a simple REST API for the user defined in an expressjs app. Here is the get() method that is routed to the /users/:id GET request.

/**
 * Get a user
 *
 * @class UsersApi
 * @method get
 * @param req {Request} The express request object.
 * @param res {Response} The express response object.
 * @param next {NextFunction} The next function to continue.
 */
public get(req: Request, res: Response, next: NextFunction) {
  //verify the id parameter exists
  const PARAM_ID: string = "id";
  if (typeof req.params[PARAM_ID] === "undefined" || req.params[PARAM_ID] === null) {
    res.sendStatus(404);
    next();
    return;
  }

  //get the id
  var id = req.params[PARAM_ID];

  //get authorized user
  this.authorize(req, res, next).then((user: IUserModel) => {
    //make sure the user being deleted is the authorized user
    if (user._id !== id) {
      res.sendStatus(401);
      next();
      return;
    }

    //log
    console.log(`[UsersApi.get] Retrieving user: {id: ${req.params.id}}.`);

    //find user
    User.findById(id).then((user: IUserModel) => {
      //verify user was found
      if (user === null) {
        res.sendStatus(404);
        next();
        return;
      }

      //send json response
      res.json(user);
      next();
    }).catch(next);
  }).catch(next);
}

Here is what I am doing in the get() method:

  • First, I verify that the id request parameter was provided. If not, the user get’s a 404 Not Found.
  • Next, I have a method named “authorize” in a base class with the following signature: authorize(req: Request, res: Response, next: NextFunction): Promise<IUserModel>. Note that it returns a Promise of our user model, IUserModel.
  • Assuming the user is authorized via a token sent in the headers with every request to the API we have a IUserModel object (similar to what we created above).
  • I think make sure that the authorized user is the one requesting to GET the user data. If not, sorry buddy, but you just get a 401 Unauthorized response.
  • Ok, now that we made sure everything is in order, let’s do a bit of logging and then get the user from mongodb. Here I use the User model to find the document based on the id parameter.
  • Next, we verify that the user document returned is not null. If it is, then we send back a 404 Not Found response.
  • Last, we return the json of the user document and call the next() method.

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).