Adwin Ang
Adwin Ang

Adwin Ang

Implementing Mongoose Models with Next.js in TypeScript

Learn how to implement mongoose models in TypeScript for Next.js that takes advantage of the type system for Development and Production.

Adwin Ang's photo
Adwin Ang
·Jul 28, 2022·

3 min read

Table of contents

  • Non Next.js application
  • Implementation for Next.js

Non Next.js application

In a typical Express JS project the below code would have sufficed

// test.model.ts
import { model, Model, Schema } from "mongoose";
import createModel from "../createModel";

interface ITest {
    first_name: string;
    last_name: string;
}

interface ITestMethods {
    fullName(): string;
}

type TestModel = Model<ITest, {}, ITestMethods>;

const testSchema = new Schema<ITest, TestModel, ITestMethods>({
    first_name: String,
    last_name: String,
});

testSchema.method("fullName", function fullName() {
    return this.first_name + " " + this.last_name;
});

export default model<ITest, TestModel>("tests", testSchema);

But if the code were to be used for development in Next.js you would get the following error

OverwriteModelError: Cannot overwrite `tests` model once compiled

Implementation for Next.js

To use Mongoose with Next JS during development requires some roundabout implementation of creating models due to the HMR (Hot Module Replacement) done by Next JS.

Common implementation found

module.exports = mongoose.models.User || mongoose.model("User", UserSchema);

One problem with the implementation is that it is a JavaScript approach and while it works in TypeScript with no error, it does not make use of the TypeScript type system.

My implementation

The implementation used the Next.js recommended way to connect to MongoDB as reference. Next.js code

// lib/createModel.ts
import { Model, model, Schema } from "mongoose";

// Simple Generic Function for reusability
// Feel free to modify however you like
export default function createModel<T, TModel = Model<T>>(
    modelName: string,
    schema: Schema<T>
): TModel {
    let createdModel: TModel;
    if (process.env.NODE_ENV === "development") {
        // In development mode, use a global variable so that the value
        // is preserved across module reloads caused by HMR (Hot Module Replacement).
        // @ts-ignore
        if (!global[modelName]) {
            createdModel = model<T, TModel>(modelName, schema);
            // @ts-ignore
            global[modelName] = createdModel;
        }
        // @ts-ignore
        createdModel = global[modelName];

    } else {
        // In production mode, it's best to not use a global variable.
        createdModel = model<T, TModel>(modelName, schema);
    }
    return createdModel;
}
// lib/models/test.model.ts
import { Model, Schema } from "mongoose";
import createModel from "../createModel";

interface ITest {
    first_name: string;
    last_name: string;
}

interface ITestMethods {
    fullName(): string;
}


type TestModel = Model<ITest, {}, ITestMethods>;

const testSchema = new Schema<ITest, TestModel, ITestMethods>({
    first_name: String,
    last_name: String,
});

testSchema.method("fullName", function fullName() {
    return this.first_name + " " + this.last_name;
});

export default createModel<ITest, TestModel>("tests", testSchema);
// pages/api/test.ts
import { connection, connect } from "mongoose";
import type { NextApiRequest, NextApiResponse } from "next";
import testModel from "../../lib/models/test.model";

const uri: string = process.env.MONGODB_URI || "";

export default async function test(
    req: NextApiRequest, 
    res: NextApiResponse
) {
    try {
        await connect(uri);
        const testObject = new testModel({ 
            first_name: "Tom", 
            last_name: "Jerry" 
        });
        // The intellisense will detect the fullName Method
        const name = testObject.fullName();
        await testObject.save();

        res.status(201).json({
            name,
        });

        // Erase test data after use
        connection.db.dropCollection(
            testModel.collection.collectionName
        );

    } catch (err) {
        res.status(400).json(err);
    }
}

Simply make any REST API request to the /api/test route to see the code in action Source Code

 
Share this