Node.js: how to create a URL shortener with ExpressJS and MongoDB

Node.js: how to create a URL shortener with ExpressJS and MongoDB

Implementing a URL shortener system is pretty easy with ExpressJS and MongoDB.

Implementing a URL shortener system is pretty easy with ExpressJS and MongoDB.

Our structure is really simple: a web client makes an authenticated POST request to an API endpoint passing the full URL to be shortened. Here we save the full URL into a database collection along with the randomly generated string that identifies such URL. Then we send back this string to the client. If the URL is already in the database, we return the stored random string immediately.

Now the client can build the short version of the URL by simply creating a new URL like http://short.localhost/u/random-string-identifier. This endpoint is a plain GET request that will make the actual HTTP redirect to the target URL.

First, we create a collection named for example urls. Each document in this collection has the following schema.

{
    target: String,
    id: String
}

target represents the original URL while id is the random string associated with it.

The first thing to do is to create a couple of helpers to manage authentication and the generation of the random string.

'use strict';

module.exports = {
    isAuthenticated(name, value, request) {
        return request.headers[name] === value;
    },
    randomString(length = 6) {
        return 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz0123456789'.split('').sort(() => {
        Math.random() - 0.5;
    }).join('').substring(0, length);
    }
};

Now we can create the route that generates the random string for the client. I'm using the general db reference to mean that you can use whatever MongoDB driver you want.

'use strict';

const app = require('express')();
const API_HEADER_VALUE = 'Do not hardcode me!';
const helpers = require('./lib/helpers');
const {isAuthenticated, randomString} = helpers;
const db = require('./lib/db');

const auth = (req, res, next) => {
    if(!isAuthenticated('X-Auth', API_HEADER_VALUE, req)) {
        return res.status(401).send({error: 'Unauthorized'});
    }
    next();
};

app.post('/shorten', auth, async (req, res) => {
        const {url} = req.body;

        try {
            const dest = await db.urls.findOne({target: url});
            if(dest) {
                res.send({id: dest.id});
            } else {
                const id = randomString();
                const newUrl = {
                    target: url,
                    id: id
                };

                db.urls.insert(newUrl);
                res.send({id: id});
            }
        } catch(err) {
            res.status(500).send(err);
        }
});


app.listen(8080);

We put the authentication logic inside a middleware function so that it's reusable on other routes of our application.

Now a client gets the short identification string associated with that URL. We can then implement the route that handles the HTTP redirect.

app.get('/u/:id', async (req, res) => {
        const {id} = req.params;
        try {
            const dest = await db.urls.findOne({id: id});
            if(dest) {
                res.redirect(dest.target);
            } else {
                res.sendStatus(404);
            }
        } catch(err) {
            res.sendStatus(500);
        }
    });

If the URL has been stored in our collection, we try to find a match with its identification string. If there's a match, we perform an HTTP redirect to the target URL.

There's a problem with our solution that it's worth mentioning: our database collection will sooner or later become very huge due to the number of records that will be stored in it.

A possible solution would be to add a timestamp field to our document schema so that it can be used as a reference for a periodical batch operation that removes obsolete or expired data.