All Posts

Building a URL Shortener Using Firebase Functions

S
Scott Hladun
February 11, 2022

What is a URL Shortener?

We've all used URLs to navigate the web to our favourite websites, share online content with friends and family, and as developers, to pass information between different parts of our websites. However, it sometimes isn't feasible to share links that store so much information, one example is on Twitter where you're limited by your character count. On other occasions it may make sense to use a cleaner branded link, especially when including it in social media posts. A URL shortener allows you to do this by giving you a shorter link that redirects users to pages they're looking for.

Nobi+ Services is a platform that helps connect students across Ontario to tutors and other service providers. To help the service providers share their profiles more easily with their networks, I'm developing a URL shortener that gives them a shorter link that looks good when posted on social media.

Getting Started

To get started we're going to setup a Firebase project that includes Cloud Firestore, Hosting and Functions. I'm not going to go into too much detail about the setup part because there are incredible tutorials available online that describe it better than I ever could.

Once you have your project ready to go, create a new folder where you will store all of your files and initialize Firebase Functions by running firebase init functions. This will setup a project structure for you to get started.

The last thing we need to do before we jump into code is install the dependecies for our project. We're going to use Express, Short ID, and Valid URL. You can install them by running the following command.

npm install express shortid valid-url

Now we're ready to begin writing our functions!

Shortening our URL

Start by creating a new folder, I've called mine routes, but you can choose any name you'd like. Inside that folder we'll create two files; url.js and redirect.js. Next, open up the url.js file and we'll begin by importing our dependencies.

const express = require('express');
const validUrl = require('valid-url');
const shortid = require('shortid');
const admin = require('firebase-admin');
// Initialize Firebase Admin so we can access our database
admin.initializeApp();
// Initialize our express router for our POST request
const router = express.Router();
// The base URL for our shortened links. I stored mine as an environment variable, but it can also be included as a string value.
const baseURL = process.env.BASE_URL;

Now we're going to get started on our POST function. This is what will be called to take in the long URL and return us with a short URL.

router.post('/shorten', async (req, res) => {
    const { longUrl } = req.body;

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

    // Generate a unique short ID for the end of our base URL
    const urlCode = shortid.generate();

    if (validUrl.isUri(longUrl)) {
        try {
            // Check if the long url is already in the database
            let url = await admin.firestore().collection('urls').where('longUrl', '==', longUrl).get();
            if (!url.empty) {
                res.status(200).json(
                    url.docs[0].data()
                );
            } else {
                // Add the url to the database
                admin.firestore().collection('urls').add({
                    longUrl,
                    shortUrl: `${baseUrl}/${urlCode}`,
                    urlCode,
                    createdAt: new Date().toISOString()
                });
                res.status(200).json({
                    original_url: longUrl,
                    short_url: `${baseUrl}/${urlCode}`
                });
            }
        } catch (err) {
            res.status(500).json({ error: 'Something went wrong' });
            console.log(err);
        }
    } else {
        res.status(401).json({ error: 'Invalid long url' });
    }
});

module.exports = router;

We start off by getting the longURL variable from the request and do a check to make sure that our base URL is a valid URL (don't forget to include the 'https'!). Then we use one of our short ID dependency to generate a unique ID for us. We then make sure that the long URL from the request is in fact a valid URL, otherwise we send a 401 response back to the client.

Next we'll wrap our function in a try/catch in case anything goes wrong. In our try statement we first check to see if the URL already has a short URL in our database (no point is saving it twice, right?). To do this we run a query in our firestore collection called 'urls'. If it finds a match, then we'll return that document to the user in a JSON object. Otherwise we'll add a brand new document in the 'urls' collection with the new link, and return the newly created short URL to the client! Congrats, you've officially created a URL shortener, but we're not going to stop here, what's the use of the short URL if it doesn't actually redirect us anywhere?

Redirecting our URL

Now we're going to open up our redirect.js file. Once again we'll start by importing the dependencies we need.

const express = require('express');
const router = express.Router();
const admin = require('firebase-admin');

Then we're going to make a GET request with the route being our short URL code.

router.get('/:code', async (req, res) => {
    try {
        const url = await admin.firestore().collection('urls').where('urlCode', '==', req.params.code).get();
        if (!url.empty) {
            console.log(url.docs[0].data());
            res.redirect(url.docs[0].data().longUrl);
        } else {
            res.status(401).json({ error: 'Invalid short url' });
        }

    } catch (err) {
        res.status(500).json({ error: 'Something went wrong' });
        console.log(err);
    }
});

module.exports = router;

You'll notice that we used a colon before the 'code' route, this lets our function know that this is a dynamic route that won't always be the same. In our try statement we query our 'urls' collection using the 'code' route parameter that we're passed from the GET request. If our database finds a match then we redirect our user to the long URL we have stored in the database. If there isn't a match for that code, then we tell the client that it isn't a valid short URL.

Exporting it to Firebase Functions

The last thing we need to do to before we can export our code to Firebase Functions, is to consolidate it all. In the index.js file that firebase init created for us, write the following code.

const express = require('express');
const functions = require('firebase-functions');

// Create a new express application instance
const app = express();

// Parse incoming requests data as JSON
app.use(express.json({
    extended: true
}));

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

exports.app = functions.https.onRequest(app);

The code above references the two files that we created and exports it nice and neatly to a https request that is ready to run in Firebase Functions! To deploy our function, run the following command in the terminal from the functions folder.

firebase deploy --only functions

To test it out your newly deployed function you can use Postman (or your favourite API platform). Firebase should have provided you a Function URL once it finished deploying, this is what you'll use to make a POST request in Postman. In the body, you'll add your 'longUrl' inside a JSON object like in the picture below.

[@portabletext/react] Unknown block type "image", specify a component for it in the `components.types` prop

When you send the request, you should be returned with two variables; short_url and original_url. Voila! Your firebase function is working!

Linking Firebase Hosting

The very last thing we're going to do is allow our website to redirect the user to the correct site when they input the shortened URL into their browser. To do this is actually quite simple, we just have to change a few lines in our firebase.json file for our hosted website.

In the rewrites section you can include the following lines.

      {
        "source": "/:code",
        "function": "app"
      },
      {
        "source": "/api/url/shorten",
        "function": "app"
      },

When the user accesses either of those links with our base url (eg. nobiinc.com/:code), it will run our firebase function that we named 'app'. This will access our database and redirect our user to the long URL!

Conclusion

Now you can create beautiful branded links that your users can share anywhere! As for Nobi+ Services, our tutors and service providers can easily share their info to their networks using a link that is clean and easy to read.