How a URL Shortener Works and How to Build One with Next.js
Published: 2023-09-11 12:55:35

In web development, CRUD (create, read, update, delete) operations are essential, but they only scratch the surface of what's possible. To build applications tailored to specific use cases, it's often necessary to create custom business logic.

This article aims to share a few skills that can help you build apps beyond creating, reading, updating, and deleting. If you’ve used AWS Amplify, you know you get a CRUD API out of the box once you create a table but I wanted to give you more than just that.

A URL shortener is an excellent example of an application that requires a custom solution. It's not just about storing and retrieving data, but also about generating a unique, shorter representation of a given URL, and redirecting users to the corresponding long URL when accessed.

By the end of this article, you should be able to write custom logic that communicates with your [Amplify GraphQL API](https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/js/). We will write the custom logic in a [Next.js API](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) which you can deploy to [AWS Amplify](https://aws.amazon.com/amplify/?trk=4301a4e1-3af3-45f9-8fdc-1400729d3f5e&sc_channel=ps&ef_id=EAIaIQobChMIk9nGi8SagAMVK0JBAh0rqAQxEAAYASAAEgL_GvD_BwE:G:s&s_kwcid=AL!4422!3!656437113991!e!!g!!aws%20amplify!20039309735!148673400379).


## Prerequisites

To follow this tutorial, you'll need an AWS account. If you don't have one, you can create an account [here](https://portal.aws.amazon.com/billing/signup?redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation#/start/email). This tutorial also assumes that you're familiar with the basics of both [JavaScript/ES6](https://developer.mozilla.org/en-US/docs/Web/JavaScript) and [React](https://react.dev/).


## Create an Amplify Project

To create an Amplify project, navigate to your AWS console, search for AWS Amplify, and select it 
to open the Amplify Console.

If this is your first Amplify app, scroll to the bottom of the page in the Amplify Studio section, select [**Get started**](https://console.aws.amazon.com/amplify/home?#/deploy-backend). Name your app, and select **Confirm Deployment**.

If you’ve created an Amplify App in the past, follow the steps below:


- Select **New app** in the upper right-hand corner, and select **Build an app** from the dropdown menu.


![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705649059_file.png)



- Give the app a name, and click the **Confirm Deployment** to deploy.


![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705659143_file.png)


Once the deployment is completed, click the **Launch Studio** button to launch the Amplify studio.


![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705669597_file.png)


AWS Amplify Studio is a visual development environment tailored for building fullstack web and mobile apps. One of its standout features is the ease with which you can set up backend resources for various tasks, such as authentication and managing customer data. 

It also has a Content Management System (CMS), which is incredibly useful for viewing and managing user data.


![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705681642_file.png)


You should now see the **Home** menu for your application. To learn more, see the [Amplify Studio introduction](https://docs.amplify.aws/console/).


## Create a Data Model

Next, we need to define a data model to store a short code and long url. To ensure that a short URL always redirects to the correct long URL, it's important to store the short-long URL pairs in a persistent location. This allows for quick retrieval of the original URL when a user navigates to the short URL, as we can efficiently query for the original URL based on the short code.

Amplify provides the ability to create data models, which represent the structure of the data your application will work with. The Studio data model designer provides a visual way to achieve this.

Follow the steps below to create a data model for your app:

- On the **Setup** menu on the left, select **Data,** and select **Add model**.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705709524_file.png)

- As seen in the image below, we call our data model **URL**. Click the **Add a field** link, and in the field that appears, and a a field name called `long` to store the long URL.
- Add another field name called `short` to store the short URL.
- Select the **Save and Deploy** button. Acknowledge the warning, and select **Deploy.**

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705729498_file.png)


The deployment should take a few minutes, but once it is completed, you should see a message saying you've successfully deployed the data model.


## Get Environment Variables

After successfully deploying our data model in the Amplify Studio, we need to set up environment variables to enable secure communication between our Next.js application and AWS AppSync back-end. For this, we need our GraphQL endpoint and the corresponding API key.

Follow these steps to get your credentials:


- Log into the **AWS Console** and navigate to the **AWS AppSync** service.
- Select the API for your application.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692703379998_file.png)

- On the left-hand side menu, click on **Settings**.
- Here, you'll find your GraphQL endpoint and your API Key.


![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692703436903_file.png)


Store these values securely. They values will be used as environment variables later in our application.


## Set up Next.js Project

To keep the focus of this guide on building our chat app, I'll skip the steps in setting up certain dependencies, such as 
[Tailwind CSS](https://tailwindcss.com/) for styling, [Nano ID](https://github.com/ai/nanoid#api) for generating strings used to create a short URL version of an original URL and [validator](https://www.npmjs.com/package/validator) for implement URL validation.

Run the following command from your preferred directory to clone a [Next.js](https://nextjs.org/) starter project that already includes these dependencies.

```sh
npx degit christiannwamba/urlshortener#starter urlshortener
```

Run the following command to navigate into the **urlshortner** directory, install the dependencies, and start up your development server:

```sh
cd urlshortner
npm install
npm run dev
```

This lets you see the output generated by the build. You can see the running app by navigating to [http://localhost:3000](http://localhost:3000/).

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705787546_CleanShot+2023-07-18+at+08.38.17.gif)

The `pages/index.js` file contains the basic structure for our URL shortener app. It imports React, and Next.js's `Link` component for navigation.

It renders an `input` field where users can enter the URL they wish to shorten, with Tailwind CSS utility classes providing styling. It also renders a section to display the original and shortened URLs. However, this is currently a static display and will be replaced with dynamic data as we build out the functionality of our app.

At the root of your project, create a file called `.env` and add the following to it:

```js
GRAPHQL_ENDPOINT=REPLACE_WITH_YOUR_GRAPHQL_ENDPOINT
GRAPHQL_KEY=REPLACE_WITH_YOUR_GRAPHQL_KEY
```

## URL Validation

When building a URL shortener, ensuring that the URLs being input are valid is important. Not every string is a valid URL, and users can make mistakes while entering URLs or even input strings that are not URLs at all. By not validating the URLs, we risk our application breaking or not behaving as expected.

In this section, we will implement URL validation using a third-party library — [validator](https://www.npmjs.com/package/validator), to help ensure the input is a valid URL. Update the code in your `pages/index.js` file to match the following:

```js
import React from "react";
import Link from "next/link";
// Import this
import isURL from "validator/lib/isURL";
function Home() {
  // Add this
  const [isURLValid, setIsURLValid] = React.useState(true);
  return (
    <main className="grid place-items-center h-screen">
      <div className="bg-cyan-900 w-full grid place-content-center py-20">
        <div>
          <h1 className="text-center text-4xl text-white">URL Shortener</h1>
          <div></div>
          <div className="w w-96">
            <div className="relative mt-4">
              {/* Update input element to match the following */}
              <input
                type="text"
                name="search"
                placeholder="Enter link here"
                id="search"
                className={`block w-full rounded-md border-0 py-3 px-3 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset outline-none focus:ring-cyan-600 ${
                  !isURLValid && "focus:ring-red-600 ring-red-600"
                } `}
                onKeyUp={async (e) => {
                  if (e.key === "Enter") {
                    if (isURL(e.target.value)) {
                      setIsURLValid(true);
                    } else {
                      setIsURLValid(false);
                    }
                  }
                }}
              />
              <div className="absolute inset-y-0 right-0 flex py-1.5 pr-1.5">
                <kbd className="inline-flex items-center rounded border border-gray-200 px-1 font-sans text-xs text-gray-400">
                  Enter
                </kbd>
              </div>
            </div>
            {true && (
              <div className="flex gap-4 mt-6 p-5 rounded-md border border-cyan-500 bg-cyan-50 items-center">
                <p className="line-clamp-1">Long URL</p>
                <p className="text-cyan-700">
                  <Link href={`/`}> lg.sh/shorturl</Link>
                </p>
              </div>
            )}
          </div>
        </div>
      </div>
    </main>
  );
}
export default Home;
```

In the code above, we import the `Link` component to handle the application's internal routing and the `isURL` function from the **validator** library. `isURL` is a function that checks if a string is a valid URL.

In the `Home` function, we initialized a state variable — `isURLValid`. It stores a boolean value and keeps track of whether the URL provided by the user is valid or not.

Next, we add the `onKeyUp` event handler to our `input` element. It checks whether the URL entered by the user is valid every time the user presses the '**Enter**' key. If the URL is valid, the `isURLValid` state is set to `true`; otherwise, it is set to `false`. We also use this validation state to dynamically style the appearance of the input field to provide real-time feedback to the user about the validity of their input.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692703913819_file.png)

## Generating the Short URL

Next, Let’s create a Next.js API route that generates the short URL. To do this, go to your `pages/api` folder and create a new file called `shorten.js`. Then add the following code to it:

```js
import { customAlphabet, urlAlphabet } from "nanoid";
export default async function handler(req, res) {
  const GRAPHQL_ENDPOINT = process.env.ADD_YOUR_API_ENDPOINT;
  const GRAPHQL_API_KEY = process.env.ADD_YOUR_API_KEY;
  
  const shortCode = customAlphabet(urlAlphabet, 5)();
  
  const query = /* GraphQL */ `
    mutation CREATE_URL($input: CreateURLInput!) {
      createURL(input: $input) {
        long
        short
      }
    }
  `;
  const variables = {
    input: {
      long: req.body.longUrl,
      short: shortCode,
    },
  };
  const options = {
    method: "POST",
    headers: {
      "x-api-key": GRAPHQL_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ query, variables }),
  };
  const response = {};
  try {
    const res = await fetch(GRAPHQL_ENDPOINT, options);
    response.data = await res.json();
    response.statusCode = 200;
    if (response.data.errors) response.statusCode = 400;
  } catch (error) {
    response.statusCode = 400;
    response.data = {
      errors: [
        {
          message: error.message,
          stack: error.stack,
        },
      ],
    };
  }
  res.status(response.statusCode).json(response.data);
}
```

Here, we are importing two specific functions from the `nanoid` library:

- `customAlphabet`: is a function that allows us to create a unique string generator, and we can specify the alphabet and size for the unique string.
- `urlAlphabet` is a predefined alphabet designed to generate unique URL-friendly strings.

We use this to generate a unique, URL-friendly string of length 5. This string is used to create the short URL version of the original URL.

The function handler is an async function that accepts a request (req) and a response (res) as parameters. In it, we defined two environment variables, `GRAPHQL_ENDPOINT`, and `GRAPHQL_API_KEY`. Replace their values with your API endpoint and API key credentials.

Next, we define a query that contains a GraphQL mutation named `CREATE_URL`. This mutation accepts an input of type `CreateURLInput`. The mutation will create a new URL entry and return its long and short values.

We define the mutation variables, including the original URL (long) and the generated short code (short). We also define the options for the fetch request to include the request method as POST, set the headers (including the API key), and convert the query and variables objects into a JSON string for the request body.

Finally, we make an HTTP request to the GraphQL endpoint with the defined options and process its response. If the request is successful, the response's `data` and `statusCode` are updated accordingly. If there are errors in the response data, the status code is set to 400.

Now update the code in your `pages/index.js` file to look like so:

```js
import React from "react";
import Link from "next/link";
import isURL from "validator/lib/isURL";

function Home() {
  const [isValidURL, setIsValidURL] = React.useState(true);

  //Add this
  const [url, setURL] = React.useState({ long: "", short: "" });

  // Add this
  async function submitUrl(url) {
    const res = await fetch("/api/shorten", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(url),
    });
    const data = await res.json();
    console.log(data);
    return data.data.createURL;
  }

  return (
    <main className="grid place-items-center h-screen">
      <div className="bg-cyan-900 w-full grid place-content-center py-20">
        <div>
          <h1 className="text-center text-4xl text-white">URL Shortener</h1>
          <div></div>
          <div className="w w-96">
            <div className="relative mt-4">
              <input
                type="text"
                name="search"
                placeholder="Enter link here"
                id="search"
                className={`block w-full rounded-md border-0 py-3 px-3 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset outline-none focus:ring-cyan-600 ${
                  !isValidURL && "focus:ring-red-600 ring-red-600"
                } `}
                onKeyUp={async (e) => {
                  if (e.key === "Enter") {
                    if (isURL(e.target.value)) {
                      setIsValidURL(true);
                      //Add these
                      const url = await submitUrl({ longUrl: e.target.value });
                      setURL(url);
                    } else {
                      setIsValidURL(false);
                    }
                  }
                }}
              />
              <div className="absolute inset-y-0 right-0 flex py-1.5 pr-1.5">
                <kbd className="inline-flex items-center rounded border border-gray-200 px-1 font-sans text-xs text-gray-400">
                  Enter
                </kbd>
              </div>
            </div>
            {/* Update this section */}
            {url.short.length > 0 && (
              <div className="flex gap-4 mt-6 p-5 rounded-md border border-cyan-500 bg-cyan-50 items-center">
                <p className="line-clamp-1">{url.long}</p>
                <p className="text-cyan-700">
                  <Link href={`/${url.short}`}> lg.sh/{url.short}</Link>
                </p>
              </div>
            )}
          </div>
        </div>
      </div>
    </main>
  );
}

export default Home;
```

We defined another state variable, `url`, to hold the long URL entered by the user and its corresponding short URL returned from the server.

Next, we define an asynchronous function to send a **POST** request to the endpoint `/api/shorten` and get the shortened URL. It accepts the long URL as an argument, sends it to the server, converts the response to JSON, and stores it in the variable `data`. The shortened URL is then returned from the function.

In the `onKeyUp` event handler added to our input element, we call the `submitUrl` function if the entered URL is valid and update the `url` state variable with the returned short URL.

We then check to see if the `url` state variable's `short` property has a length greater than 0 (indicating that a short URL has been generated). If it does, it is displayed along with the original URL on the screen. Finally, we use the `Link` component from Next.js to create a link that leads to the short URL. We used `lg.sh` as a placeholder for the shortened URLs. Feel free to replace this with your own domain if you have one.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705889905_CleanShot+2023-07-18+at+08.41.29.gif)

If you head back to the Amplify Studio Console for you app, on the **Setup** menu on the left, select **Content**. You should see the long URL and a short representation of that URL as shown below.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692706031848_file.png)

Now, let us create the logic that will handle redirection from the short URL to the long URL when a user clicks on the short URL.

## Convert a Short URL to a Long URL

Now, we need to create a dynamic route in our application that will handle the redirection from the shortened URL to the long URL. The idea is that when a user opens the shortened URL, they are redirected to the original, long URL.

In your pages folder, create a dynamic page called `[short.js]` and add the following to it:

```js
import React from "react";

function Short() {
  return <div></div>;
}

export async function getServerSideProps(context) {
  console.log(context.params.short);
  const GRAPHQL_ENDPOINT = process.env.GRAPHQL_ENDPOINT;
  const GRAPHQL_KEY = process.env.GRAPHQL_KEY;
  const query = /* GraphQL */ `
    query LIST_URLS($input: ModelURLFilterInput!) {
      listURLS(filter: $input) {
        items {
          long
          short
        }
      }
    }
  `;
  const variables = {
    input: { short: { eq: context.params.short } },
  };
  const options = {
    method: "POST",
    headers: {
      "x-api-key": GRAPHQL_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ query, variables }),
  };
  const res = await fetch(GRAPHQL_ENDPOINT, options);
  const data = await res.json();
  const url = data.data.listURLS.items[0];
  console.log(url.long);
  return {
    redirect: {
      destination: url.long,
    },
  };
}
export default Short;
```

In Next.js, every file in the `pages` directory corresponds to a route. In this case, we created a dynamic page and exported a component —`Short`, but we do not want to display this page to the user. We want the server to immediately redirect them to the original long URL. To accomplish this, we need to find the short code from the request path, look up the corresponding long URL in our database, and then redirect that long URL before any page is sent to the client.

This needs to happen on the server, so we are using the `getServerSideProps` function and Next.js's built-in redirection to perform a redirect based on the database data. The `getServerSideProps` function accepts a `context` object as an argument that contains several properties, including query parameters.

We define a GraphQL query to list all URLs that match a specific condition. It'll search for the URL with the same short code as the one in the `context.params.short`.

Next, we prepare our `variables` object for the GraphQL query. The `input` parameter is an object that filters the URLs we are querying for by the `short` property, and we want the URL where the `short` property equals `context.params.short`.

Next, we prepare our `variables` object for the GraphQL query. The `input` parameter is an object that filters the URLs we are querying for by the `short` property, and we want the URL where the `short` property equals `context.params.short`.

Afterward, we define the `options` object for the fetch request, and it includes setting the HTTP method as **POST**, specifying the appropriate headers, and then stringifying the query and variables objects to be sent in the body.

Next, we make the fetch call to the GraphQL endpoint and wait for the response. Once we get the response, we parse it as JSON. We get the first URL that matches our query from the list of URLs retrieved, but we expect only one item since the short code should be unique.

Finally, we use the redirect property in the returned object from the `getServerSideProps` function to redirect the client to the original long URL associated with the short code.

To see your newly created short URL in action, head to your browser and type in a valid URL. Once you hit Enter, you should see your short URL displayed on your screen. Click on the short URL to be redirected to the original long URL.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705955347_CleanShot+2023-07-18+at+08.43.33.gif)

## Host Your App with AWS Amplify

You need to [deploy your project a git provider such as Github](https://docs.github.com/en/migrations/importing-source-code/using-the-command-line-to-import-source-code/adding-locally-hosted-code-to-github). With a git repository, you can use Amplify Hosting to set up continuous delivery automatically. Simply commit all your changes to Git and push your website to your repository. Amplify Hosting will take care of the rest, deploying your website to the cloud and updating it whenever you push changes to your repository.

Go to your AWS console and search **AWS Amplify** and select it from the list of services.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705161505_file.png)

Next, select the **urlshortner** app and select **Hosting environments**.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705267466_file.png)

Select GitHub or your Git provider on the next page and click the **Connect branch** button.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705322791_file.png)

Select the repository you intend to host, select the main branch, and click the **Next** button to proceed.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705387962_file.png)

In the build settings page, select an environment or Create a new environment. Select an existing service role if you have one, or click the **Create new role** button to create a new role that allows Amplify Hosting to access your resources.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705423604_file.png)

For security reasons, Amplify needs you to be explicit about the fact that you want to include environmental variables in a build. 

The `printev` command prints all of the current environment variables. Then grep matches the two you want and adds those to the `.env.production` file.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705451278_file.png)

Click the **Edit button** and edit the build settings to include the following command:

```js
printenv | grep -e GRAPHQL_ENDPOINT -e GRAPHQL_KEY >> .env.production
```

Once you’re done, click the **Save** button.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705515401_file.png)

Scroll down to the **Advanced settings** section and add your environmental variables.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705543276_file.png)

Review your settings on the next screen and click the **Save and deploy** button to start the deployment processes. Now you can sit back and watch your app get deployed. This might take a few minutes, depending on your internet connection. 

After deploying, click on the live link provided by Amplify to access your app.

![](https://paper-attachments.dropboxusercontent.com/s_A16E70A4C6D432D62FBDAB98A4947DB0A8DB224B50C7A84FA27D13C4A60CBDD8_1692705567779_file.png)

## Clean Up

To ensure that you don’t have any unused resources in you AWS account, run the following command to delete all the resources that were created in this project if you don’t intend to keep them.

```sh
amplify delete
```

## **Conclusion**

This article guides you through building a URL shortener using AWS Amplify and Next.js. With lengthy URLs becoming increasingly difficult to share and remember, URL shorteners offer a user-friendly solution. With your URL shortener up and running, you can start using it to simplify your links and make your content more accessible to your audience.