The MERN Stack - a web technology stack consisting of MongoDB, Express.js, React, and Node.js - was introduced around 2018 as a popular set of technologies to enable end-to-end development in JavaScript. Although the popularity of fullstack frameworks such as Next.js _mean_s that MERN isn't as popular as it once was, it is still a viable and productive choice with its decoupled architecture providing more flexibility than you'll get with a fullstack framework.
Today, I'd like to introduce the TERN (Tigris, Express.js, React, and Node.js) stack which replaces MongoDB with Tigris.
In this post, you'll learn about MERN and TERN and why you should migrate from MERN to TERN. We'll cover all the steps involved in migrating a MERN application to TERN (Tigris, Express.js, React, and Node.js); first using Tigris MongoDB compatibility to get your existing application working with Tigris instead of MongoDB without any code changes (⚠️ spoiler: it's as simple as updating the MongoDB connection string). Then, we'll make a few changes to get the application using the Tigris SDK instead of the MongoDB Node.js driver, completing the migration.
What is MERN?
MERN (MongoDB, Express.js, React, and Node.js) is an alternative to the MEAN (MongoDB, Express.js, Angular, and Node.js). MEAN was introduced in 2013 and MERN followed as React became increasingly popular in 2018.
The MERN stack consists of:
- Browser: React with either JavaScript or TypeScript (transpiled to JavaScript)
- Server: Express.js with either JavaScript or TypeScript (transpiled to JavaScript)
- Database: MongoDB most likely using the MongoDB Node.js driver, although an ODM such asMongoose maybe used.
What is TERN?
TERN (Tigris, Express.js, React, and Node.js) takes the ideas behind MERN and replaces MongoDB with Tigris.
- Browser : React with either JavaScript or TypeScript (transpiled to JavaScript)
- Server : Express.js with either JavaScript or TypeScript (transpiled to JavaScript)
-
Database :Tigris Database using theTigris TypeScript SDKwhich provides an in-built ODM.
Why migrate from MERN to TERN?
So, why would you migrate from MERN (MongoDB) to TERN (Tigris)? Tigris is a serverless NoSQL database and search platform and an alternative to MongoDB.
Tigris has several benefits over MongoDB. Tigris:
- Takes a code-first approach to database schema modelingandsearch index modeling.
- Supports database branchingto fit into your development workflow.
- Removes the need for manual cluster provisioning and shard management.
- Supports ACID transactions without caveats.
- Provides the same full-text search functionality whether you are using Tigris Cloud or self-hosting. TheMongoDB text searchfunctionality differs between MongoDB Atlas and self-hosted MongoDB.
- Has been built with a modern cloud-native architecture, meaning the running costs are lower than MongoDB.
Convinced? If yes, fantastic! If not, let us know why.
How to migrate from MERN to TERN
To follow along, you'll need the following:
- Node.js LTS or above installed
- A Tigris Cloud account or a self-hosted instance of Tigris. This tutorial will assume you're using Tigris Cloud.
For this tutorial, we'll use a forked and slightly updated version of the MongoDB MERN example used in the official MongoDB MERN stack guide.
The updates that have been applied are to use the newest version of the MongoDB Node.js driver (for MongoDB 6.0+ wire protocol support), remove the unused Mongoose ODM dependency, update the code to use Promises, define the database name in an environment variable, use encodeURIComponent
when passing parameters in URLs from the client, and add alt
tags to images (not essential for this tutorial, but it felt like the right thing to do).
To follow along, clone the updated TERN stack example repo and switch to the mern
branch:
git clone https://github.com/tigrisdata-community/tern-stack-example.gitcd tern-stack-examplegit checkout mern
If you have a MongoDB instance running, you can try the MERN example out by following the instructions in the README.
We can now begin the migration.
You can migrate from MERN to TERN (and from any MongoDB application to Tigris) in either a one or two-step process.
The two-step process, and the process covered here, is:
Using Tigris MongoDB Compatibility
We recently released Tigris MongoDB compatibility in beta which allows you to connect to Tigris using the MongoDB 6.0+ wire protocol. From a MERN perspective, this probably means an application that is using the MongoDB Node.js driver.
Updating the application we've just cloned to use Tigris MongoDB Compatibility is as simple as updating the connection string. So, head to the Tigris Console and create a new project. You'll then land in your project's Getting Started section. From there, click the MongoDB Compatibility tab and the MongoDB connection string that you see on that tab.
Either update or create a mern/server/config.env
file, setting the value of ATLAS_URI
to the Tigris MongoDB connection string. Also, set DATABASE_NAME
to the name of the Tigris project you just created. Your file will look similar to the following:
mern/server/config.env
ATLAS_URI=mongodb://{TIGRIS_CLIENT_ID}:{TIGRIS_CLIENT_SECRET}@m1k.preview.tigrisdata.cloud:27018?authMechanism=PLAIN&tls=trueDATABASE_NAME=mern-to-tern
Then, follow the existing README instructions to start the server and the client.
In one terminal, start the server with:
cd mern/server
npm install
npm start
Server command line output
$ npm start
> server@1.0.0 start
> node server.js
Server is running on port: 5000
Successfully connected to MongoDB.
In another terminal, start the client with:
cd mern/client
npm install
npm start
Client command line output
Note: Some other dependencies could do with being updated.
Compiled with warnings.
Warning
(3769:3) autoprefixer: Replace color-adjust to print-color-adjust. The color-adjust shorthand is currently deprecated.
Search for the keywords to learn more about each warning.
To ignore, add // eslint-disable-next-line to the line before.
WARNING in ./node_modules/bootstrap/dist/css/bootstrap.css (./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[1].oneOf[5].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[1].oneOf[5].use[2]!./node_modules/source-map-loader/dist/cjs.js!./node_modules/bootstrap/dist/css/bootstrap.css)
Module Warning (from ./node_modules/postcss-loader/dist/cjs.js):
Warning
(3769:3) autoprefixer: Replace color-adjust to print-color-adjust. The color-adjust shorthand is currently deprecated.
webpack compiled with 1 warning
When you run the client, the browser automatically opens the browser. By default, the app will be running on http://localhost:3000
. Try out the application to see it in action.
Please remember that all we've done to make this application work with Tigris Database is updated the connection string to point to Tigris Cloud.
Migrating from the MongoDB Node.js Driver to the Tigris SDK
Tigris MongoDB compatibility is a great first step in migrating a MERN application. But, to take full advantage of TERN and Tigris, it's recommended that the application is updated to use the Tigris TypeScript SDK. There's more work required to do this, but the changes are pretty small, as this section will show.
Since the client and server are decoupled via the API that the Express.js application exposes, we only need to update the code for the Express.js API endpoints.
Update dependencies
Let's begin by updating the MERN application dependencies to use those required by TERN; remove the mongodb
driver and add @tigrisdata/core
:
npm uninstall monodbnpm i @tigrisdata/core
Add Tigris Configuration
Update the mern/server/config.env
file to contain additional Tigris config:
mern/server/config.env
- ATLAS_URI=mongodb+srv://<username>:<password>@sandbox.jadwj.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
- DATABASE_NAME=mern-to-tern
PORT=5000
+ TIGRIS_CLIENT_ID={TIGRIS_CLIENT_ID}
+ TIGRIS_CLIENT_SECRET={TIGRIS_CLIENT_SECRET}
+ TIGRIS_PROJECT=mern-to-tern
+ TIGRIS_URI=api.preview.tigrisdata.cloud
+ TIGRIS_DB_BRANCH=main
Replacing {TIGRIS_CLIENT_ID}
and {TIGRIS_CLIENT_SECRET}
with real values from your Tigris project application keys.
Define your database schema in TypeScript
Install TypeScript as a development dependency:
npm i -D typescript
Add the following to a mern/server/tsconfig.json
:
mern/server/tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
Define a schema in TypeScript to match the auto-generated schema that was created when using the application via Tigris MongoDB compatibility:
import {
Field,
PrimaryKey,
TigrisCollection,
TigrisDataTypes,
} from "@tigrisdata/core";
@TigrisCollection("records")
export class Record {
@Field({ elements: TigrisDataTypes.STRING })
$k?: string[];
@PrimaryKey(TigrisDataTypes.BYTE_STRING, { order: 1, autoGenerate: true })
_id?: string;
@Field()
name!: string;
@Field()
position!: string;
@Field()
level!: string;
}
The @TigrisCollection("records")
decorator and supplied records
value informs Tigris that there is a records
collection of documents of type Record
in the structure defined by that class.
The fields _id
, name
, position
, and level
, are all used within the application. The $k
field isn't used but is required to allow the application to continue to work with the auto-generated MongoDB compatibility schema. As you'll see, all the application's fields are of type string
.
TIP: You can also grab this schema from the Tigris Console under Your Project -> Database -> click on the records schema -> select TypeScript from the drop down:
With the schema in place, we need to validate it with Tigris and use it with the Tigris TypeScript SDK. To do that, create a mern/server/script/setup.ts
script with the following contents:
mern/server/scripts/setup.ts
import { Tigris } from "@tigrisdata/core";
import { Record } from "../db/record";
import dotenv from "dotenv";
dotenv.config({ path: "./config.env" });
async function main() {
// setup client
const tigrisClient = new Tigris();
// ensure branch exists, create it if it needs to be created dynamically
await tigrisClient.getDatabase().initializeBranch();
// register schemas
await tigrisClient.registerSchemas([Record]);
}
main()
.then(async () => {
console.log("Setup complete ...");
process.exit(0);
})
.catch(async (e) => {
console.error(e);
process.exit(1);
});
In the above code, the config is loaded using dotenv
, and a Tigris
client is instantiated and assigned to the tigrisClient
variable, which uses the loaded environment variables. Then, the database and database branch are initialized by tigrisClient.getDatabase().initializeBranch()
. Finally, register the Record
schema via tigrisClient.registerSchemas([Record])
.
Update the package.json
to make use of the setup script:
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
+ "setup": "npx ts-node scripts/setup.ts",
+ "prestart": "npm run setup",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
+ "@tigrisdata/core": "^1.0.0-beta.44",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
- "mongodb": "5.2",
- "mongoose": "^5.12.4"
+ "reflect-metadata": "^0.1.13"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.4"
}
}
This adds a setup
NPM script utilized by prestart
, which is automatically before the script defined in the start
NPM script.
Note: the above diff also shows the other changed dependencies.
With the schema defined, we can move on to updating the application code.
Update the connection code
Next, update the connection code defined in mern/server/db/conn.js
by removing the MongoDB Node.js driver and updating the code to use the Tigris TypeScript SDK:
mern/server/db/conn.js
- const { MongoClient } = require("mongodb");
- const Db = process.env.ATLAS_URI;
+ const { Tigris } = require("@tigrisdata/core");
- const client = new MongoClient(Db, {
- useNewUrlParser: true,
- useUnifiedTopology: true,
- });
+ const client = new Tigris();
var _db;
module.exports = {
connectToServer: async function (callback) {
try {
- const db = await client.connect();
- _db = db.db(process.ENV.DATABASE_NAME);
- console.log("Successfully connected to MongoDB.");
+ _db = await client.getDatabase();
+ console.log("Successfully connected to Tigris.");
return callback();
} catch (err) {
return callback(err);
The Tigris
client is instantiated and automatically uses the environment variables we've defined in the mern/server/config.env
file.
The other difference worth pointing out is that we don't name the database that is being used in client.getDatabase()
since each Tigris Project has only one database.
Common MERN to TERN code changes
A consistent difference across all of the Express.js route changes are:
- The MongoDB
ObjectId
is no longer used - The query contains a
filter
to identify the document. So, from{ _id: new ObjectId(req.params.id) }
to{ filter: { _id: req.params.id } }
is a consistent code change in all routes - Collections are accessed via
getCollection(collectionName)
instead ofcollection(collectionName)
Migrate MERN database read to TERN database read
Update the GET /record
route:
mern/server/routes/record.js
- const ObjectId = require("mongodb").ObjectId;
// This section will help you get a list of all the records.
recordRoutes.route("/record").get(async function (req, res) {
- let db_connect = dbo.getDb("employees");
- const result = await db_connect.collection("records").find({}).toArray();
+ let db_connect = dbo.getDb();
+ const result = await db_connect.getCollection("records").findMany().toArray();
return res.json(result);
});
To retrieve multiple documents using the Tigris SDK usefindMany
, passing no parameters. This returns a cursor that exposes a toArray
function to get an Array
of results.
If you restart the server application, you'll see the main application page displaying the employees but via a call to GET /record
which now uses the Tigris SDK.
Migrate MERN database create to TERN database create
Update the POST /record/add
route:
Note: To be more faithful to REST, this should be POST /record
mern/server/routes/record.js
recordRoutes.route("/record/add").post(async function (req, res) {
let db_connect = dbo.getDb();
let myobj = {
name: req.body.name,
position: req.body.position,
level: req.body.level,
};
- const result = db_connect.collection("records").insertOne(myobj);
+ const result = await db_connect.getCollection("records").insertOne(myobj);
res.json(result);
});
In this case, the method signature forinsertOne
stays the same.
Migrate MERN database update to TERN database update
Change the POST /update/:id
route:
Note: To be more RESTful, this should be PATCH /record/:id
orPATCH /record/:id
if it replaces the whole resource.
mern/server/routes/record.js
recordRoutes.route("/update/:id").post(async function (req, res) {
let db_connect = dbo.getDb();
- let myquery = { _id: new ObjectId(req.params.id) };
- let newvalues = {
+ const myquery = {
+ filter: { _id: req.params.id },
+ fields: {
$set: {
name: req.body.name,
position: req.body.position,
level: req.body.level,
},
+ },
};
- const result = await db_connect
- .collection("records")
- .updateOne(myquery, newvalues);
+ const result = await db_connect.getCollection("records").updateOne(myquery);
console.log("1 document updated");
res.json(result);
});
The updateOne
Tigris SDK function differs from the MongoDB Node.js driver in that, it takes a single parameter with the following properties to achieve the same result:
-
filter
the filter used to find the single document to be updated -
fields
where this property can use the$set
syntax supported by the MongoDB driver.
Migrate MERN database delete to TERN database delete
Finally, update the DELETE /:id
route:
Note: REST police: this should be DELETE /record/:id
mern/server/routes/record.js
recordRoutes.route("/:id").delete(async (req, res) => {
let db_connect = dbo.getDb();
- let myquery = { _id: new ObjectId(req.params.id) };
- const result = await db_connect.collection("records").deleteOne(myquery);
+ let myquery = { filter: { _id: req.params.id } };
+ const result = await db_connect.getCollection("records").deleteOne(myquery);
res.json(result);
});
As discussed, the only change here is the filter
required by the Tigris SDK.
Update the logo
Let's add the Tigris logo 😄 Grab the Tigris green logo, save it to tern/client/public
and updatemern/client/src/components/navbar.js
as follows:
mern/client/src/components/navbar.js
<NavLink className="navbar-brand" to="/">
<img
- alt="MongoDB logo"
+ alt="Tigris logo"
style={{ width: 25 + "%"}}
- src="https://d3cy9zhslanhfa.cloudfront.net/media/3800C044-6298-4575-A05D5C6B7623EE37/4B45D0EC-3482-4759-82DA37D8EA07D229/webimage-8A27671A-8A53-45DC-89D7BF8537F15A0D.png"
+ src="/tigris-logo-green.png"
></img>
</NavLink>
Run the TERN application
And with those updates applied, the MERN application is converted to the TERN stack 🎉
Since there are no functional changes to the front-end (only the logo update), you only need to stop and start the server only to see the changes in action:
npm start
And you'll see the following output:
npm start
> server@1.0.0 prestart
> npm run setup
> server@1.0.0 setup
> npx ts-node scripts/setup.ts
info - Using reflection to infer type of Record#$k
info - Using reflection to infer type of Record#name
info - Using reflection to infer type of Record#position
info - Using reflection to infer type of Record#level
info - Using Tigris at: api.preview.tigrisdata.cloud:443
info - Using database branch: 'main'
event - Creating collection: 'records' in project: 'mern-to-tern'
Setup complete ...
> server@1.0.0 start
> node server.js
info - Using Tigris at: api.preview.tigrisdata.cloud:443
Server is running on port: 5000
Successfully connected to Tigris.
Navigate to the client URL, which by default is http://localhost:3000
, and try out the TERN app (we did refresh the client so the Tigris logo is present in this video):
If you have a look at the Tigris Consoleand explore the data, as shown in the video, you'll see the _id
values are structured differently, and the $k
value is not populated for documents created via the Tigris SDK.
Besides the logo, the application looks and performs exactly as the MERN application did. However, as the intro outlines, you can now also take advantage of the benefits of using Tigris.
You can find all the code changes applied to migrate the MongoDB MERN application to TERN in this diff.
Next Steps
How about raising a pull request to add full-text search to the TERN application using Tigris Search?
What should we call the Tigris version of MEAN? No, I've got it; it has to be NEAT!
Join the Tigris Discord and let us know what you think of TERN and the process of migrating a MERN (or any other MongoDB) application over to TERN.
Tigris is an open-source serverless NoSQL database and search platform. If you have any questions, or you'd like to contribute to the Tigris open source project.
Top comments (3)
Why still ExpressJS is in the center of the developers attention when we can replace it with a more reliable option like NestJS? Just kinda curious :thinking_face:
Because Express JS and NestJS is a different tools for different cases. Also NestJS doesn't have any option which can beat Express.
Sorry but I am afraid that I cannot follow you, May I ask you kindly enlighten me how they are different tools? And also It is just like C++ and Node.js relationship. Node.js leverages C++ to gave us a higher level tool to code and develop faster. We can say the same thing about the relationship between Node.js, ExpressJS, and NestJS.