Sling Academy
Home/Node.js/How to set up and use Mongoose with TypeScript

How to set up and use Mongoose with TypeScript

Last updated: December 30, 2023

Overview

Are you looking to leverage TypeScript’s strong-typing with Mongoose for MongoDB? TypeScript allows for easier development of complex MongoDB-based applications by catching errors at compile-time with static types. This tutorial aims to guide you through the process of setting up a TypeScript Node.js environment and integrating Mongoose, the popular object-modeling package for MongoDB.

Prerequisites:

  • Basic knowledge of TypeScript and Node.js
  • Node.js and npm installed on your machine
  • A MongoDB server available for connection

Setting up TypeScript

First, let’s set up a new TypeScript project:

mkdir mongoose-typescript-project
cd mongoose-typescript-project
npm init -y
tsc --init

This will create a new directory, initiate a new npm project, and generate a basic tsconfig.json file, configuring your project for TypeScript.

Let’s edit the tsconfig.json to suit our needs:

{
    "compilerOptions": {
        "target": "es2021",
        "module": "commonjs",
        "lib": ["esnext"],
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
}

Integrating Mongoose

Install Mongoose and its TypeScript definitions:

npm install mongoose
npm install -D @types/mongoose

Now, set up Mongoose within a simple Node.js application:

import mongoose from 'mongoose';

mongoose.connect('mongodb://localhost/test', {
    useNewUrlParser: true,
    useUnifiedTopology: true
});

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
db.once('open', function() {
    // we're connected!
    console.log('Connected to the database.');
});

Defining a Model with TypeScript

Let’s define a Mongoose schema for a User model with TypeScript interfaces:

import mongoose, { Schema, Document } from 'mongoose';

interface IUser extends Document {
    username: string;
    email: string;
    password: string;
}

const userSchema: Schema = new Schema({
    username: { type: String, required: true },
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true }
});

const UserModel = mongoose.model('User', userSchema);

We just created a strongly-typed Mongoose model by using an interface that extends Mongoose’s Document.

Advanced Queries and Operations

Take advantage of TypeScript’s intellisense to perform operations safely:

const findUser = async (username: string) => {
    try {
        const result = await UserModel.findOne({ username }).exec();
        if (result?._id) {
            console.log(`User found: ${result?.toJSON()}`);
        } else {
            console.log('No user found');
        }
    } catch (error) {
        console.error('Error finding user:', error);
    }
};

findUser('johndoe').then(() => process.exit());

Utilizing Middlewares and Plugins

Use TypeScript to enhance Mongoose’s functionality with middlewares and plugins:

userSchema.pre('save', async function(next) {
    if (this.isModified('password')) {
        const hashed = await bcrypt.hash(this.password, 10);
        this.password = hashed;
    }
    next();
});

userSchema.plugin(somePluginAsAFunction);

Be sure that the plugins or middleware you create or use are compatible with TypeScript’s static type system.

Interfacing with Complex Data Types

Handle more complex data structures with deeper object models:

interface ILocation extends Document {
    city: string;
    country: string;
}

const locationSchema: Schema = new Schema({
    city: { type: String },
    country: { type: String }
});

userSchema.add({ location: locationSchema });

We add a nested schema to allow our user to have an associated location object.

Types & Hooks

Taking full advantage of TypeScript typings across Mongoose hooks is particularly powerful:

userSchema.post('save', (doc) => {
    console.log(`User ${doc.username} has been saved with id: ${doc._id});
});

This hook logs after a user is saved, with access to TypeScript typings ensuring an id property creation after saving.

Conclusion

Throughout this walkthrough, we seamlessly combined Mongoose’s schema-based approach to MongoDB with TypeScript’s powerful type system, creating a robust and maintainable codebase. From setting up the project to creating web-hooks, we harnessed TypeScript’s full potential to keep our data access layer type-safe, organized, and scalable. Implementing Mongoose with TypeScript invites fewer runtime errors and more developer productivity, leading to a superior development experience.

Next Article: Mongoose: How to compile a model from a schema

Previous Article: Mongoose: How to Connect to Self-Hosted MongoDB (3 Examples)

Series: Mongoose.js Tutorials

Node.js

You May Also Like

  • NestJS: How to create cursor-based pagination (2 examples)
  • Cursor-Based Pagination in SequelizeJS: Practical Examples
  • MongooseJS: Cursor-Based Pagination Examples
  • Node.js: How to get location from IP address (3 approaches)
  • SequelizeJS: How to reset auto-increment ID after deleting records
  • SequelizeJS: Grouping Results by Multiple Columns
  • NestJS: Using Faker.js to populate database (for testing)
  • NodeJS: Search and download images by keyword from Unsplash API
  • NestJS: Generate N random users using Faker.js
  • Sequelize Upsert: How to insert or update a record in one query
  • NodeJS: Declaring types when using dotenv with TypeScript
  • Using ExpressJS and Multer with TypeScript
  • NodeJS: Link to static assets (JS, CSS) in Pug templates
  • NodeJS: How to use mixins in Pug templates
  • NodeJS: Displaying images and links in Pug templates
  • ExpressJS + Pug: How to use loops to render array data
  • ExpressJS: Using MORGAN to Log HTTP Requests
  • NodeJS: Using express-fileupload to simply upload files
  • ExpressJS: How to render JSON in Pug templates