This page looks best with JavaScript enabled

Shorten URL with Express and AlpineJS

 ·   ·  ☕ 10 min read

In this post let us see how we can leverage the power of Express with a sprinkling of Alpine JS to create a quick URL shortener application.

But, why?

Express is the most popular server-side framework and my “go to” choice for creating anything really quick. Building a front-end for Express is as easy as using handlebars (or anything really) that goes in HTML served by Express. But that lacks a “nice” user experience.

AlpineJS is a small and sweet framework that will go places. It follows Vue in many respects (and tries to achieve parity in a lot of respects), but does not want to be a full-scaled framework. Rather, it sits happily with other code to provide user interactivity to your front-end.

Depending on what you are set to do, AlpineJS can be a good choice for your back-end application.

Use Case: Shorten URL

The premise is simple.

  1. User supplies URL (e.g. http://twitter.com/techformist)
  2. We shorten it and give it back to user (e.g. https://go.co/tf, assuming go.co is our domain)
  3. Anyone can use the short URL. The request just bounces off our server to navigate to the longer web address

For simplicity we will ignore any authentication requirements.

We will use ExpressJS as the backend because that is one of the quicker and nicer frameworks to work with. We will use SQLite as the database.

Create a Basic Express App with SQLite DB

We will start building with some basic code. No generators, no fluff.

Create a new folder for your project and initialise using npm.

1
2
3
mkdir shortu
cd shortu
npm init -y

npm init will create package.json in your project folder. Edit the file to include scripts to run your application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  //  ...

  "scripts": {
    "run": "node server",
    "dev": "nodemon server"
  }

  // ...
}

Install express, sqlite3 and a few useful libraries for security.

1
npm i --save express express cors helmet sqlite3 yup

Install nodemon so that our express application gets restarted automatically every time there is a change. This is useful during development.

1
npm i --save-dev nodemon

Create a new file server.js - this will be our main Express file. Let us bring in express and friends.

1
2
3
4
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const yup = require("yup");

Include logic to initialise database.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const sqlite3 = require("sqlite3");
const db = new sqlite3.Database("./db/data.sqlite", (err) => {
  if (err) throw err;
  console.log("Connected to the SQLite database.");

  db.run(
    "CREATE TABLE IF NOT EXISTS urls (slug VARCHAR(100) PRIMARY KEY, url VARCHAR(255), clicks INTEGER)",
    (res, err) => {
      if (err) next(err);
    }
  );
});

SQLite is just a file at heart (which database isn’t?). The above logic checks for a file called db/data.sqlite. If the file does not exist, SQLite takes care of creating a new file. db.run is used to run a SQL statement.

We will execute an SQL to create a table called urls if it doesn’t already exist. There are three columns in this table -

  • url - long form URL supplied by user
  • slug - key part of short-form URL. Can be supplied by user or we generate one
  • clicks - no. of clicks on the short URL

Initialise express.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const app = express();

// middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.static("./public"));

// listener
app.listen(3000, () => {
  console.log("Listening on port 3000.");
});

Include a test statement to show a message for a request.

1
2
3
app.post("/*", (req, res) => {
  res.json({ message: "Hello, world" });
});

.. and an error function to process any errors anywhere.

1
2
3
4
// error handling
app.use((err, req, res, next) => {
  res.status(500).json({ message: err.message });
});

The basic Express application is now ready!

You can run the application using -

1
npm run dev

This should print a message - Listening on port 3000.

Send a POST request using your favourite API testing tool to see "Hello, world" message in response.

Define APIs in your Express Application

Before we go ahead with the API request/response, let use define a valid structure for the expected input.

1
2
3
4
5
// schema defn.
const schema = yup.object().shape({
  url: yup.string().url().required(),
  slug: yup.string(),
});

We will have three services -

  1. Create new “short URL”
  2. Navigate user to the long URL when a valid short URL is provided
  3. List existing short URLs

This is also a good time to remember that we need to generate a random string if user does not provide the “slug” for the short URL. This can be done in a number of ways.

We will use a package called crypto-random-string. Let us install the package.

1
npm i --save crypto-random-string

Alternatively, you can use -

Let us start coding in the services.

Create new “short URL”

Create a new short URL for the user-provided URL.

  1. The generated short URL will be the combination of our domain (where the Express application is running) + a short slug.
  2. Slug is provided in request. It is optional, generate a URL-safe slug if not provided
  3. Store long form URL and the slug in the database
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
app.post("/new", async (req, res, next) => {
  // get url and slug from input
  let { url, slug } = req.body;
  console.log("req.body: ", req.body);

  try {
    await schema.validate({ url, slug });

    if (!slug) {
      // generate slug if not provided by user
      const cryptoRandomString = require("crypto-random-string");
      slug = cryptoRandomString({ length: 10, type: "url-safe" }).toLowerCase();
    }

    // insert everything in DB
    db.exec(
      `INSERT INTO urls(url, slug , clicks) VALUES('${url}', '${slug}', 0)`,
      (err) => {
        console.log("err: ", err);
        if (err) next(err);

        // response message has the short URL
        res.json({
          shorturl: `http://${req.headers.host}/${slug}`,
          slug: slug,
          url: url,
        });
      }
    );
  } catch (e) {
    next(e);
  }
});

It is time to test this out!

Send a request to http://localhost:3000/new like so -

1
2
3
4
{
  "url": "http://google.com",
  "slug": "ro0wgfvwxg"
}

You should receive this beautiful response -

1
2
3
4
5
{
  "url": "http://google.com",
  "slug": "ro0wgfvwxg",
  "shorturl": "http://localhost:3000/ro0wgfvwxg"
}

Your short URL generation is ready, onwards to doing something with this request.

With the previous service the user received this short URL http://localhost:3000/ro0wgfvwxg, which can now be happily shared by her to thousands of people.

When these people use the URL in a browser, the request is sent to our server. This can be fulfilled by a service that will fetch the long-form URL from the given short URL, and automatically navigate users to the correct destination.

This is easier than it looks.

1
2
3
4
5
6
7
8
9
app.get("/:id", (req, res) => {
  // id = slug. e.g. `ro0wgfvwxg`
  // fetch data from DB

  db.get(`SELECT * FROM urls where slug='${req.params.id}'`, (err, data) => {
    // navigate user to the right URL
    res.redirect(data.url);
  });
});

The above code should do the trick. But, let’s improve that a bit more -

  • add error handling - navigate user to an error page if slug is not found
  • add logic to count number of clicks. Each time anyone uses the URL, the number of clicks will be incremented
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
app.get("/:id", (req, res) => {
  db.get(`SELECT * FROM urls where slug='${req.params.id}'`, (err, data) => {
    if (err) next(err);
    if (!data) res.redirect(`/?error=${req.params.id} not found!`);
    else res.redirect(data.url);

    // add clicks
    db.exec(
      `UPDATE urls SET clicks = clicks + 1 WHERE slug='${req.params.id}'`,
      (err) => {
        if (err) console.error(err);
      }
    );
  });
});

Now, try navigating to https://localhost:/ro0wgfvwxg in the browser. You should be navigated to the correct URL http://google.com.

List URLs

Let us code the last of the services - list all URLs in the database.

This service is similar to the previous one, except that we run a simpler query to fetch all URLs and return them to the caller.

1
2
3
4
5
6
7
8
9
app.get("/list", (req, res) => {
  db.all(`SELECT * FROM urls`, (err, data) => {
    if (err) next(err);

    res.json({
      data: data,
    });
  });
});

Try it! A request to https://localhost:3000/list should return all the URLs from the database.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "data": [
    {
      "slug": "ro0wgfvwxg",
      "url": "http://google.com",
      "clicks": 3
    },
    {
      "slug": "whphznmvcf",
      "url": "http://bing.com",
      "clicks": 0
    }
  ]
}

Your backend application is now fully ready to take on the world.

Front-end for Shorten URL Application

Create a simple HTML file index.html in the project root, which will contain the whole of our frontend. Include AlpineJS from CDN.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=400px, initial-scale=1.0" />
    <title>Shortu</title>

    <script
      src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js"
      defer
    ></script>
  </head>
  <body>
    Frontend Does Not Rock
  </body>
</html>

Let us code some CSS since no one likes to see default styling. Since I am too lazy to do the complete styling by myself (capability aside), I will use a simple CSS framework called “MVP.css”.

Include MVP.css and a custom CSS (for minimal style changes) in the HTML head.

1
2
<link rel="stylesheet" href="https://unpkg.com/mvp.css" />
<link rel="stylesheet" href="./assets/custom.css" />

Create folders in the project root.

1
2
3
mkdir public && cd public && mkdir assets
cd assets
touch custom.css

We can use powerful functions to bind HTML elements to backend. Let us create a form and the necessary objects that will act as the container for all functionality.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<body x-data="process()">
  <h1 class="title">Shortu URL Shortener</h1>
  <div class="subtitle">
    Input the long URL, hit "Shorten" and see magic happen.
  </div>

  <form class="input-group">
    <input type="text" placeholder="Provide the long URL" x-model="url" />
    <br />
    <input
      type="text"
      class="form-control"
      placeholder="Desired short url phrase (optional)"
      x-model="slug"
    />
  </form>

  <script>
    function process() {
      return {
        url: "",
        slug: "",
      };
    }
  </script>
</body>

Note that -

  • process() will provide the infrastructure for AlpineJS. You can define JS variables, functions and more within process function
  • x-model binds the HTML element to the backend variable (for e.g. url). This is similar to v-model in Vue

Express serves the HTML file by default when we navigate to https://localhost:3000/. Navigate to this URL in the browser to see your application in action.

Let us add functionality to allow user to send request to Express and see results.

Add a button that sends data to backend Express, and elements to show results (or error). Input below code before the ending </form> element.

1
2
3
4
5
6
7
8
<button x-on:click.prevent="shortenURL()">Submit</button>
<div x-show="error" x-on:click=" error = ''">
  <span x-text="error" class="error"></span></div>
<div id="results" x-show="data['shorturl']" class="results">
  <div class="label">Your short URL:</div>
  <h3 style="display: inline-block;" x-text="data.shorturl"></h3>
</div>

Add the corresponding functions and variables in process.

  • data to store the response URL list and error to store error
  • shortenURL - a function that invokes our new service to shorten a given URL

The complete <script> element is below -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<script>
  function process() {
    return {
      error: "",
      data: {},
      url: "",
      slug: "",
      shortenURL: async function () {
        try {
          console.log("this.url", this.url);
          const result = await fetch(`/new/`, {
            method: "POST",

            headers: {
              Accept: "application/json",
              "Content-Type": "application/json",
            },
            body: JSON.stringify({ url: this.url, slug: this.slug }),
          });

          const resJson = await result.json();
          console.log("resJson: ", resJson);

          if (result.ok) {
            console.log("result.ok: ", result.ok);

            this.data = resJson;
            this.error = "";
          } else {
            console.log("in error");
            this.data = {};
            this.error = resJson["message"];
          }
          console.log("data: ", this.data);
          console.log("error: ", this.error);
        } catch (e) {
          console.log("error", e);
          this.error = e.message;
        }
      },
    };
  }
</script>

Voila, your application is ready just like that.

url-shortener-express-alpinejs

Complete code is at this Github repo.

The End

Building applications with Express is fun, and libraries like Alpine make the user experience fun too. I have to say - I am quite smitten this smart library.

Stay in touch!
Share on

Prashanth Krishnamurthy
WRITTEN BY
Prashanth Krishnamurthy
Technologist | Creator of Things