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