Back

Building an URL Shortener with Bun

Building an URL Shortener with Bun

Long URLs pose several drawbacks, including inconvenience in sharing due to their cumbersome nature, difficulty in readability, unattractive aesthetics, and potential negative impacts on SEO. To enhance user experience and mitigate these challenges, you need a URL-shortening service like bit.ly. In this article, you will learn how to build a URL shortener of your own using Bun, Elysia, and MySQL.

A URL shortener is a service that converts a long URL into a shorter URL. The conversion process typically combines URL redirection with a unique identifier system.

When a user submits a long URL to your URL shortener, the service generates a unique identifier representing the original URL. This identifier is appended to your shortener’s domain name, forming a new, shortened URL.

For example, a long URL like https://www.example.com/article/how-url-shorteners-work might be shortened to https://short.url/abc123.

When a user clicks on the shortened link, their browser sends a request to your shortener. When your shortener receives the request, it extracts the unique identifier and then looks up the corresponding long URL associated with that identifier in its database.

Once the long URL is retrieved, your server will issue a redirection response to the user’s browser, instructing it to navigate to the original URL.

Additionally, URL shorteners often offer additional features such as analytics, allowing users to track the number of clicks on their shortened links and other relevant metrics.

Setting Up Your Development Environment

As mentioned earlier, you will build a URL shortener using Bun, Elysia, and MySQL in this tutorial. In this section, you will create and initialize a new project, connect to your database, and create the data models for your URL shortener.

First, create your project directory by running the command below:

mkdir url-shortener && cd url-shortener

Next, initialize Bun in your application by running the command below:

bun init

Next, run the command below to install the dependencies required by your project:

bun add elysia prisma mysql2 nanoid @prisma/client dotenv

The packages you installed above include the following:

  • Elysia: Elysia is a performance-focused HTTP framework for Bun, similar to Express but 21x faster.
  • Prisma: Prisma is an object-relational mapper (ORM) for JavaScript and TypeScript.
  • MySQL2: MySQL2 is a MySQL client for Node.js applications.
  • Nanoid: Nanoid is a compact and secure unique string generator. You will need this package to identify each URL uniquely.
  • Dotenv: Dotenv is an NPM package that loads environmental variables from a .env file into process.env.

Finally, create a src folder in your project’s root directory. This is where you will store all your application’s source files. Create your service and controller files in your src folder, shortener.service.ts and shortener.controller.ts, respectively.

Connecting to Your Database

After installing the required packages, run the command below to initialize Prisma in your project.

npx prisma init

Running the above command initializes Prisma in your application and creates a prisma folder, a .env file, and a .gitignore in your root directory. Inside the prisma folder, you will find a schema.prisma file.

The schema.prisma file contains information on how your application interacts with Prisma, including a data source and a generator.

The data source specifies how Prisma should connect to your database.

By default, the data source provider is set to postgresql. Change it to mysql, as shown in the code block below:

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

Next, open your .env file and replace the dummy DATABASE_URL with your actual MySQL database connection string.

Creating Your Data Models

Next, add the code block below to your schema.prisma file to create your Url data model:

model Url {
   id        Int      @id @default(autoincrement())
   longUrl   String
   shortUrl  String
   uniqueId  String
   createdAt DateTime @default(now())
}

The code block above represents your Url model. It contains information you need to make your shortening service work, including the longUrl, which is the long URL provided by the user, shortUrl, which is the shortened form of the longUrl (a combination of your base URL and a unique identifier for the long URL), the uniqueId, and the createdAt date.

Next, run your migration to sync your database by running the command below:

npx prisma migrate dev --name init

Running the command above will sync your database with the models in your prisma.schema file.

Note: You can replace init with any name of your choice.

Now that you have set up your development environment, created your data model, and synced the model to your database, you will implement your URL-shortening login.

Implementing Your URL-Shortening Logic

In this section, you will implement the logic required to shorten a URL.

To shorten a URL, first, you must validate if the data you received from the user is valid to ensure that you only shorten valid URLs.

There are various ways to validate if a given string is a valid URL, including using Regex or third-party packages like class-validator. In this tutorial, you will validate the URL using Regex.

To validate the URL using Regex, create a utils.ts file and add the code block below to it:

//utils.ts
export function isURLValid(url: string): boolean {
  const urlPattern: RegExp = /^(http|https):\/\/[^ "]+$/;

  return urlPattern.test(url);
}

The function isURLValid in the code block above validates a given string against the provided urlPattern. urlPattern is a regex expression that matches strings that start with either “http” or “https”, followed by a colon and two forward slashes, and then followed by one or more characters that are not spaces or double quotes (in other words, a URL pattern).

Next, add the following imports to your shortener.service.ts.

import { PrismaClient } from "@prisma/client";
import { nanoid } from "nanoid";
import { isURLValid } from "./utils";

The imports above include PrismaClient for creating a connection between your application and Prisma, nanoid for generating a unique ID, and isURLValid for checking if a given string is valid.

After validating the given string, you need to check if your application has already shortened the URL to avoid duplicate short URLs. You can do this by querying your database for a record with the matching URL in the request payload.

Like so:

const urlExists = await prisma.url.findFirst({
  where: {
    longUrl,
  },
});

if (urlExists) {
  return urlExists.shortUrl;
}

If your application has shortened the URL, return the short URL to the user; otherwise, you must create a short URL for the long URL.

A short URL is made up of two parts: your application’s base URL and a unique identifier.

url

Your application’s base URL is the domain or subdomain on which your shortener is hosted, along with any additional path components if necessary. During development, your base URL is “localhost:<PORT>”. You can store your base URL in your .env file for more flexibility, especially when deploying the application to different environments (e.g., development, staging, production).

Like so:

BASE_URL = "http://localhost:3000"

The unique identifier for the short url should be random and predictable. It also needs to be as short as possible while maintaining uniqueness; ideally, it should be around 7-10 characters.

You can generate a unique ID by calling the nanoid function. By default, calling nanoid will generate a unique ID with 21 characters. However, you can shorten it by passing your preferred length as an argument.

For example:

nanoid(10)

After generating the unique ID, concatenate it with your application’s base URL.

Like so:

 const shortUrl = `${baseURL}/${uniqueId}`;

Next, create an instance of your Url model and save it to your database.

Like so:

const url = await prisma.url.create({
  data: {
    longUrl,
    shortUrl,
    uniqueId,
  },
});

Finally, return the short URL to the user.

You can implement the logic above by adding the code block below to your shortener.service.ts file:

//shortener.service.ts
export async function shortenUrl(longUrl: string) {
  try {
    //Is the Url valid?
    const isUrlValid = isURLValid(longUrl);

    if (!isUrlValid) {
      throw new Error("Invalid URL. Please enter a valid URL and try again");
    }

    //Has url already been shortened
    const urlExists = await prisma.url.findFirst({
      where: {
        longUrl,
      },
    });

    //Return already shortened URL
    if (urlExists) {
      return urlExists.shortUrl;
    }

    //Your base URL
    const baseURL = process.env.BASE_URL;

    //Unique identifier for the short URL
    const uniqueId = nanoid(10);

    //short url
    const shortUrl = `${baseURL}/${uniqueId}`;

    //Save to your database
    const url = await prisma.url.create({
      data: {
        longUrl,
        shortUrl,
        uniqueId,
      },
    });

    //Return the short url
    return {
      message: "URL shortened successfully",
      shortUrl: url.shortUrl,
    };
  } catch (error) {
    throw error;
  }
}

The function above takes in a string as an argument, validates it as a URL, shortens it, and returns it to the user.

Now you are done implementing the service layer of your URL shortening logic; you will implement your controller layer.

Implementing Controller Logic

Your controller for shortening URLs should handle POST requests to ‘/shorten’. When the controller receives the request, it should extract the long URL from the request body (or context object) and pass it as an argument to the shortenUrl function you implemented earlier.

To implement your controller logic, add the code below to your shortener.controller.ts file:

//shortener.controller.ts
import { Elysia } from "elysia";
import { shortenUrl } from "./shortener.service";

export const urlController = (app: Elysia) => {
  app.post("/shorten", async (context: any) => {
    const { longUrl } = context.body;

    const shortUrl = await shortenUrl(longUrl);

    return shortUrl;
  });
};

Now that you are done with fully implementing your shortening logic, you will implement your redirection logic.

Implementing Redirection Logic

In this section you will implement the logic required to redirect a user to their original URL using the shortened URL.

To redirect a user to the long URL using the shortened URL, you have to extract the unique ID from the short URL, fetch the long URL from your database, and redirect the user with a status code of 301 (Permanent Redirect). Redirecting the user with a 301 status code helps maintain SEO integrity.

Implementing Service Layer

For the redirection logic, your service layer should handle fetching the long URL that matches the unique ID provided by the client when they use the short URL.

To implement the logic above, add the code block below to your shortener.service.ts file:

//shortener.service.ts
export async function fetchUrl(urlUniqueId: string) {
  try {
    const url = await prisma.url.findFirst({
      where: {
        uniqueId: urlUniqueId,
      },
    });

    return url;
  } catch (error) {
    throw error;
  }
}

The code block above searches your database for a URL with a matching unique ID and return it.

Next, you will implement your controller layer logic for redirection.

Implementing Controller Layer

Your controller for redirection will handle GET requests made to your base URL with the /:uniqueId as a path parameter. When your controller receives the request, it should extract the unique ID from the params object on the context object and pass it as an argument to the fetchUrl function you created earlier.

To implement this functionality, first, update the imports from your service file to include fetchUrl in your controller file like so:

//shortener.controller.ts
import { shortenUrl, fetchUrl } from "./shortener.service";

Then, add the code block below to your urlController function directly under the route handler for /shorten.

//shortener.controller.ts
...

app.get("/:uniqueId", async (context) => {
  const uniqueId = context.params.uniqueId;

  const url = await fetchUrl(uniqueId);

  if (url) {
    context.set.status = 301;
    context.set.redirect = url.longUrl;
  } else {
    context.set.status = 404;
    return {
      message: "Page not found",
    };
  }
});

...

The code block above extracts a shortened URL’s unique ID from the request params and passes it as an argument to the fetchUrl function. If the fetchUrl function returns a URL, it sets the status code to 301 using Elysia’s set function and redirects the user to the long URL using the same set function. If no record of the url is found in your database, it returns a 404 status code (Not Found) and a message to the user.

Now that you’re done with your URL shortening and redirection logic, you need to create a server that will listen to the requests made to your application.

Creating Your Bun-Elysia Server

In this section, you will create a web server to listen for incoming requests made to your application.

To create a Bun-Elysia Server, first, add the following imports to your index.ts file:

import * as dotenv from "dotenv";
dotenv.config();
import Elysia from "elysia";
import { urlController } from "./src/shortener.controller";

The imports above include dotenv, which you imported and configured. It also includes Elysia and your urlController.

Next, add the code block below to your index.ts file to create a new Elysia app instance, mount your controller, and listen for traffic on port 3000.

//index.ts
const app = new Elysia();

app.use(urlController as any);

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

Now, your URL shortener is complete. Next, you will deploy it to the cloud using Render’s free hosting service and Docker to make it accessible to your users.

Deploying your URL Shortener

In this section you will deploy your URL shortener on Render using Docker.

Before deploying your application on Render using a docker container, you need a Dockerfile.

Run the command below to create a Dockerfile:

touch Dockerfile

Then, add the code block below to your Dockerfile:

FROM oven/bun

WORKDIR /app

COPY ./prisma prisma
COPY package.json .
COPY bun.lockb .

RUN bun install

COPY . .

EXPOSE 3000

CMD ["bun", "index.ts"]

The Dockerfile above sets up a Docker image for a Bun application, installs dependencies, and sets up the container to run the application with the bun command and index.ts file as an argument.

Next, create and push your code to a remote repository for your code on GitHub or GitLab.

Deploying on Render

To deploy your application on Render, you need an active account; you can sign up for free if you do not have one.

Note: To follow along with this deployment guide, your URL shortener must be available on GitHub/GitLab.

Next, log in to your account and navigate to your dashboard. On your dashboard, click “Web Services”.

Web services

Next, select the option to “Build and deploy from a Git repository”, as shown in the image below, then press the next button.

Deployment Method

Next, connect your GitHub/GitLab, select the URL shortener repository you want to deploy, and press connect.

Select Repository

Next, fill out the required details in the configuration and select “Free” as your instance type.

Configure Web Service

Next, scroll down, fill in your environmental variables, and click the “Create Web Service” button, as shown in the image below:

ENV Variables

Clicking the “Create Web Service” button will take you to the deployment page, where you can monitor the deployment process through the logs, as shown in the image below.

After a while, you should get a log saying, “Your service is live 🎉”. You can access the live version of your service using the URL provided by Render on the top left corner of your screen, as shown in the image below.

Deployed App

The default domain assigned by Render is too long for a URL shortener; be sure to add your short custom domain to your web application before using it publicly. After changing your default domain to your custom domain, be sure to update the value to your BASE_URL in your environmental variables to match your new domain.

Conclusion

In this article, you learned how a URL shortener works, how to build it using the fast JavaScript runtime known as Bun, and how to deploy it on Render using Docker.

You can test your application using an API testing tool like cURL or Insomnia. Alternatively, you can build a UI for your URL shortener to make it easier for users to interact with it.

Scale Seamlessly with OpenReplay Cloud

Maximize front-end efficiency with OpenReplay Cloud: Session replay, performance monitoring and issue resolution, all with the simplicity of a cloud-based service.

OpenReplay