DEV Community

The Ghost Dev
The Ghost Dev

Posted on

MERN URL Shortener App - Part 2

In this multi-part tutorial, we will work together to build an URL Shortener app (basic version of bitly) using React, NodeJS, Express and MongoDB. This will be basically a full-stack application build with MERN stack.

We will be learning Context API, Hooks, Express router, and building a Custom API With JWT Authentication.

On our first part, we successfully installed all modules and completed setup of our Express and MongoDB. Currently we have our Express server running and connected to our MongoDB Atlas database.

In this part, we will setup Mongoose model for URL, and Express routers for generating short URL from long URL and successful redirection.

Let's jump in then...

Step 4 - Setting up Mongoose model for MongoDB

Mongoose models are database schema constructors. Models are responsible for creating and reading documents from the underlying MongoDB database.

Though this is a very small app, but to keep scalability in mind, we will structure the application as such that all the similar config files are stacked together in separate directories.

So let's create a directory models in our root directory, and inside that models directory create a file Url.js. This file will store the database schema which we will use to read and modify data from the database specific to only urls.

Let's setup our URL schema by editing the Url.js file:

const mongoose = require('mongoose');

const UrlSchema = new mongoose.Schema({
  urlCode: {
    type: String,
  },
  longUrl: {
    type: String,
    required: true,
  },
  shortUrl: {
    type: String,
    required: true,
  },
  date: {
    type: String,
    default: Date.now,
  },
});

module.exports = mongoose.model('url', UrlSchema);

To explain the above code:

  1. We are calling mongoose module in the mongoose variable.
  2. Creating a new variable UrlSchema which is constructing a new Mongoose schema.
  3. We are adding all the fields or tables inside this schema:

    • urlcode: This field will store the short id for the url
    • longUrl: This field will store the actual URL sent by client
    • shortUrl: This field will store the shorten URL generated by our application
    • data: This field will store the date and time when the url is generated

    Then we are exporting this model using module.exports = mongoose.model('url', UrlSchema);. Here url is our database collection name, which will be created automatically on first request to the database.

Step 5 - Setting up Express routes

To keep consistency on our application workflow, We will now create a new directory routes in our root directory and inside that a file genurl.js for configuring our Express routes.

Before we get onto our routes, we need to setup our Base URL in our config file, which is default.json inside the config directory.

{
  "mongouri": "mongodb+srv://priyajit:indiatimes@cluster0.bb04n.mongodb.net/urlshortner?retryWrites=true&w=majority",
  "baseURI": "http://localhost:5000"
}

Note: The Base URL will be the main URL of the short URL that we will be generating. In our case, we are using our default Node URL as our Base URL, but in production this needs to be replaced with an actual URL (i.e https://bit.ly/)

We can now edit the route file genUrl.js and create our first route.

First we will bring in all our required modules:

const express = require('express');
const router = express.Router();
const validUrl = require('valid-url');
const shortid = require('shortid');
const config = require('config');

then we will bring in the Mongoose model for Url:

const Url = require('../model/Url');

Now we will setup our route, which will be a POST request and this route will be accessible to all.

First, we will create a POST function to get the data from client-end.

router.post('/', async (request, response) => {
  // We will handle the requested data here

});

module.exports = router;

Inside the above function, we will do all our data handling. First, we will destructure and fetch the URL sent from client-end.

const { longUrl } = request.body;

Then, we will get our Base URL and will validate the same using our valid-url module, to check if the base URL is an FQDN:

const baseUrl = config.get('baseURI');

if (!validUrl.isUri(baseUrl)) {
  return res.status(401).json('Invalid base url');
}

We will now generate a short code, which along with the Base URL will identify the long URL and will redirect to it.

const urlCode = shortid.generate();

now we will validate the long URL sent from client-end. If it validates we will generate and store the short URL to the database, and if it doesn't then we will return an error.

if (validUrl.isUri(longUrl)) {
  // We will generate short URL here

} else {
  res.status(401).json('Invalid Long Url');
}

We will now generate short URL inside the above conditional statement. The steps will be:
1. we will first check if the long URL sent from client-end already exists in our database.
2. if it does exist, we will get the short URL from the database and will send it back as a response.
3. if it doesn't exist, we will create a variable shortUrl and store the new short URL, by concatenating our Base URL and our short code.
4. then we will use our Mongoose Url model and store all the required data to our database, and then we will send the short URL as a response back to the client-end.

This is how we can achieve all of the above steps:

try {
      let url = await Url.findOne({ longUrl });

      if (url) {
        res.json(url);
      } else {
        const shortUrl = baseUrl + '/' + urlCode;

        url = new Url({
          longUrl,
          shortUrl,
          urlCode,
          date: new Date(),
        });

        await url.save();

        res.json(url);
      }
    } catch (err) {
      console.error(err.message);
      res.status(500).json('Server Error');
    }

Note: As I am using Async/Await to deal with Promises, I have used Try/catch block. You can of course use .then() and .catch(), if you want to.

As route is now in place, we can edit our server.js file to call this route whenever an HTTP POST request is sent to http://localhost:5000/api/genurl

Let's edit our server.js file now to call the above route.

app.use('/api/genurl', require('./routes/genurl'));

Step 6 - Testing with Postman

  1. Add http://localhost:5000/api/genurl/ as URL and set the request type to POST

Alt Text

  1. Add Content-Type to application/json in Headers

Alt Text

  1. Add the URL you want inside Body as json object, where key will be longUrl and value is an URL that you want to shorten.

Alt Text

et voilà - you get everything as json response.

Alt Text

But shortUrl will not load the longUrl just now, because we haven't added any route for that.

Step 7 - ShortURL to LongURL Redirection

Let's set that up now. And for that, we need to edit our server.js file to include below line to add our route first.

app.use('/', require('./routes/redirect'));

This will return an error as we haven't created our route file yet

Now create redirect.js file in our routes directory and add the following:

const express = require('express');
const router = express.Router();
const Url = require('../model/Url');

router.get('/:code', async (req, res) => {
  try {
    const url = await Url.findOne({ urlCode: req.params.code });

    if (url) {
      return res.redirect(url.longUrl);
    } else {
      return res.status(404).json('No Url found');
    }
  } catch (err) {
    console.error(err.message);
    res.status(500).json('Server Error');
  }
});

module.exports = router;

Now let's iterate over what are we doing with the above codes:

  1. First, we are calling in Express and initializing router
  2. then we are calling in our Mongoose Url model
  3. next we are creating a GET function with :code as our parameter, which will have the urlCode sent from shortUrl
  4. now, in a Try/Catch block we are requesting complete URL data where urlCode in our database url collection matches the sent urlCode.
  5. then in a conditional block, if urlCode matches, we will fetch the longUrl and will redirect to it, and if not, we will send an error as a response, that No Url found.
  6. and if any error happens while connecting to the database or fetching from the database, we will send a Server error as a response.

We are now all set and can copy hit the shortUrl in a browser and it will redirect to it's longUrl.

In the next part, we will setup React and build a frontend to generate short URL from long Url.

Follow me on Twitter and feel free to drop me any suggestion or just to say Hi!

Top comments (2)

Collapse
 
nicolewiegand7 profile image
Nicole Wiegand • Edited

Hi, I've been following along with the first two parts of this tutorial, and it's a great way to practice working with the MERN stack so far! However, there are a couple of mistakes in the instructions in this section I wanted to let you know about. First, the instructions tell you to create a "models" directory:

  • So let's create a directory models in our root directory, and inside that models directory create a file Url.js

But then in the code, it's looking for Url.js in the "model" directory:

const Url = require('../model/Url');

The second issue is in genUrl.js. You're passing in response here:
`router.post('/', async (request, response) => {
// We will handle the requested data here

});`

But then the code that gets pasted in here refers to res instead of response in multiple places, like this:

if (url) {
res.json(url);
} .....

The last issue exists in server.js. The route is looking for routes/genurl.js but it should be looking for routes/genUrl.js:

app.use('/api/genurl', require('./routes/genurl'));

After I fixed these issues, everything is working nicely and I'm looking forward to moving forward with the next installment. Hope this information helps!

Collapse
 
the_ghost_dev profile image
The Ghost Dev

yes, I made those mistakes in "models" and "genUrl" part is. Best practice is to use camel-case , so using "genUrl" would be best. About "res" and "response", I normally use short names wherever possible, like "res", "err", "req" so on and so forth, but when I was writing this tutorial, I decided to use full-name, but forgot to make those changes in the actual code.

I am really happy that you're following the tutorial and pointed my mistake. I will correct the mistakes you pointed. I got little tied up on a couple of back to back projects, but I will write and publish the frontend part soon.

Feel free to reach out to me over my email (you can find it on my profile), for any questions regarding MERN Stack. Thank You!!