How a URL Shortener Works and How to Build One with Next.js

Christian Nwamba - Sep 11 '23 - - Dev Community

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. We will write the custom logic in a Next.js API which you can deploy to AWS Amplify.

Prerequisites

To follow this tutorial, you'll need an AWS account. If you don't have one, you can create an account here. This tutorial also assumes that you're familiar with the basics of both JavaScript/ES6 and React.

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. 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.

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

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

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.

You should now see the Home menu for your application. To learn more, see the Amplify Studio introduction.

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.

  • 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.

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.

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

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 for styling, Nano ID for generating strings used to create a short URL version of an original URL and validator for implement URL validation.

Run the following command from your preferred directory to clone a Next.js starter project that already includes these dependencies.



npx degit christiannwamba/urlshortener#starter urlshortener


Enter fullscreen mode Exit fullscreen mode

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



cd urlshortner
npm install
npm run dev


Enter fullscreen mode Exit fullscreen mode

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

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:



GRAPHQL_ENDPOINT=REPLACE_WITH_YOUR_GRAPHQL_ENDPOINT
GRAPHQL_KEY=REPLACE_WITH_YOUR_GRAPHQL_KEY


Enter fullscreen mode Exit fullscreen mode

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, to help ensure the input is a valid URL. Update the code in your pages/index.js file to match the following:



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;


Enter fullscreen mode Exit fullscreen mode

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.

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:



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);
}


Enter fullscreen mode Exit fullscreen mode

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:



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;


Enter fullscreen mode Exit fullscreen mode

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.

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.

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:



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;


Enter fullscreen mode Exit fullscreen mode

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.

Host Your App with AWS Amplify

You need to deploy your project a git provider such as 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.

Next, select the urlshortner app and select Hosting environments.

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

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

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.

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.

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



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


Enter fullscreen mode Exit fullscreen mode

Once you’re done, click the Save button.

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

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.

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.



amplify delete

Enter fullscreen mode Exit fullscreen mode




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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .