Build a Simple Next.js Interactive URL Shortener App

Written by

in

In today’s digital landscape, links are everywhere. From social media posts to email signatures, we encounter them constantly. But long, unwieldy URLs can be a real pain. They’re difficult to share, can break in emails, and simply look unprofessional. That’s where URL shorteners come in, transforming lengthy addresses into concise, manageable links. In this tutorial, we’ll dive into building a simple, yet functional, URL shortener application using Next.js, a powerful React framework for building web applications.

Why Build a URL Shortener?

Creating a URL shortener is an excellent project for several reasons:

  • Practical Application: It solves a real-world problem, making it a useful tool for anyone who shares links online.
  • Learning Opportunity: It allows you to explore core web development concepts like form handling, API interactions, and data storage.
  • Scalability: While we’ll start simple, the project can be easily expanded to include features like custom short links, analytics, and user accounts.
  • Next.js Fundamentals: It provides hands-on experience with Next.js features such as server-side rendering, API routes, and dynamic routing.

Prerequisites

Before we begin, make sure you have the following:

  • Node.js and npm (or yarn): You’ll need these to manage project dependencies. Install them from nodejs.org.
  • A Code Editor: Visual Studio Code, Sublime Text, or any editor you prefer.
  • Basic Knowledge of JavaScript and React: Familiarity with these will be helpful.

Setting Up the Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app url-shortener-app

This command sets up a basic Next.js application with all the necessary configurations. Navigate into your project directory:

cd url-shortener-app

Now, start the development server:

npm run dev

Your application should now be running on http://localhost:3000. Open this in your browser to see the default Next.js welcome page.

Project Structure

Let’s take a quick look at the project structure. The key directories and files we’ll be working with are:

  • pages/: This directory contains your application’s pages. Each file in this directory represents a route. For example, pages/index.js will be the homepage.
  • pages/api/: This directory will hold our API routes, which will handle the shortening and redirection logic.
  • components/ (Optional): We’ll create this directory to store reusable React components.
  • styles/: This will be where our CSS files are located.
  • package.json: This file lists your project’s dependencies and scripts.

Creating the UI

First, we’ll build the user interface for our URL shortener. This will consist of a form where users can input a long URL and a display area to show the shortened URL.

Let’s modify pages/index.js. Replace the existing code with the following:

import { useState } from 'react';

export default function Home() {
  const [longUrl, setLongUrl] = useState('');
  const [shortUrl, setShortUrl] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    setError('');
    setShortUrl('');

    try {
      const response = await fetch('/api/shorten', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ longUrl }),
      });

      const data = await response.json();

      if (response.ok) {
        setShortUrl(data.shortUrl);
      } else {
        setError(data.message || 'An error occurred.');
      }
    } catch (err) {
      setError('An error occurred during the request.');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="container">
      <h1>URL Shortener</h1>
      <form onSubmit={handleSubmit}>
        <label htmlFor="longUrl">Enter URL:</label>
        <input
          type="url"
          id="longUrl"
          value={longUrl}
          onChange={(e) => setLongUrl(e.target.value)}
          required
        />
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Shortening...' : 'Shorten'}
        </button>
        {error && <p className="error">{error}</p>}
        {shortUrl && (
          <div>
            <p>Shortened URL: <a href={shortUrl} target="_blank" rel="noopener noreferrer">{shortUrl}</a></p>
          </div>
        )}
      </form>
      <style jsx>{`
        .container {
          display: flex;
          flex-direction: column;
          align-items: center;
          padding: 2rem;
          font-family: sans-serif;
        }

        form {
          display: flex;
          flex-direction: column;
          width: 80%;
          max-width: 400px;
        }

        label {
          margin-bottom: 0.5rem;
        }

        input {
          padding: 0.5rem;
          margin-bottom: 1rem;
          border: 1px solid #ccc;
          border-radius: 4px;
        }

        button {
          padding: 0.75rem 1rem;
          background-color: #0070f3;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }

        button:disabled {
          opacity: 0.6;
          cursor: not-allowed;
        }

        .error {
          color: red;
          margin-top: 1rem;
        }
      `}</style>
    </div>
  );
}

This code does the following:

  • Imports useState: This hook is used to manage the state of our component.
  • Defines State Variables: longUrl stores the URL entered by the user, shortUrl stores the shortened URL, isLoading indicates if the shortening process is in progress, and error displays any error messages.
  • handleSubmit Function: This function is called when the form is submitted. It prevents the default form submission behavior, sets isLoading to true, and then calls the /api/shorten API route (which we’ll create later) to shorten the URL.
  • Form and Input Fields: A simple form with an input field for the long URL and a submit button.
  • Conditional Rendering: The short URL and error messages are conditionally rendered based on the state.
  • Inline Styles: Basic styling for the UI. You can customize this in the styles/globals.css file or by using a CSS-in-JS solution like styled-components.

Creating the API Route

Next, we’ll create the API route that will handle the shortening of the URL. Create a new file named pages/api/shorten.js and add the following code:


const { nanoid } = require('nanoid');

export default async function handler(req, res) {
  if (req.method === 'POST') {
    const { longUrl } = req.body;

    if (!longUrl) {
      return res.status(400).json({ message: 'URL is required' });
    }

    try {
      const shortId = nanoid(6);
      const shortUrl = `${req.headers.origin}/${shortId}`;

      // In a real application, you would store the longUrl and shortId in a database.
      // For this example, we'll use a simple in-memory object.
      const urlMap = {
        [shortId]: longUrl,
      };

      // Simulate saving to a database (in a real app, use a database like MongoDB or PostgreSQL)
      // In a real application, you would save this to a database.
      // For this example, we'll use a simple in-memory object.
      // await saveToDatabase(shortId, longUrl);

      return res.status(200).json({ shortUrl });
    } catch (err) {
      console.error(err);
      return res.status(500).json({ message: 'Failed to shorten URL' });
    }
  } else {
    res.setHeader('Allow', ['POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

This code does the following:

  • Imports nanoid: We use this library to generate unique short IDs. Install it with: npm install nanoid
  • Handles POST Requests: It checks if the request method is POST.
  • Validates Input: It checks if a longUrl was provided in the request body.
  • Generates a Short ID: It generates a unique short ID using nanoid(6).
  • Constructs the Shortened URL: It creates the shortened URL using the server’s origin and the generated short ID.
  • Simulates Database Storage: It creates an in-memory object (urlMap) to store the mapping between the short ID and the long URL. Important: In a production environment, you would use a database like MongoDB, PostgreSQL, or a similar solution to persist this data.
  • Returns a JSON Response: It returns a JSON response containing the shortened URL.
  • Handles Errors: Includes error handling for invalid requests and internal server errors.
  • Handles Non-POST Requests: Returns a 405 Method Not Allowed if the request method is not POST.

Implementing Redirection

Now, we need to implement the redirection logic. When a user visits the shortened URL, they should be redirected to the original long URL. Create a new file named pages/[shortId].js (the square brackets indicate a dynamic route) and add the following code:


export async function getServerSideProps(context) {
  const { shortId } = context.query;

  // In a real application, you would fetch the long URL from a database.
  // For this example, we'll use the in-memory object.
  const urlMap = {
    // Replace this with your actual data retrieval from the database
    // Example:  'abcdef': 'https://www.example.com'
  };

  const longUrl = urlMap[shortId];

  if (!longUrl) {
    return {
      notFound: true,
    };
  }

  return {
    redirect: {
      destination: longUrl,
      permanent: true,
    },
  };
}

export default function RedirectPage() {
  return null; // This component doesn't render anything.
}

This code does the following:

  • Uses getServerSideProps: This function is used for server-side rendering and allows us to fetch data before the page loads.
  • Extracts the shortId: It extracts the shortId from the URL parameters using context.query.
  • Fetches the Long URL: It retrieves the corresponding longUrl from the in-memory urlMap. Important: Replace the placeholder with your database lookup logic.
  • Handles Non-Existent URLs: If the shortId is not found in the urlMap, it returns a 404 Not Found error.
  • Redirects the User: If the longUrl is found, it redirects the user to the longUrl using a 301 permanent redirect.
  • Returns null: The RedirectPage component itself doesn’t render anything, as the redirection is handled by getServerSideProps.

Testing the Application

Now, let’s test our URL shortener:

  1. Start your Next.js development server if it’s not already running (npm run dev).
  2. Open your browser and navigate to http://localhost:3000.
  3. Enter a long URL in the input field.
  4. Click the “Shorten” button.
  5. If everything is working correctly, you should see a shortened URL displayed.
  6. Copy the shortened URL and paste it into your browser. You should be redirected to the original long URL.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid them:

  • Incorrect API Route Path: Double-check that the form in pages/index.js is correctly sending the data to the /api/shorten route.
  • Missing Dependencies: Ensure you’ve installed all the necessary dependencies (e.g., nanoid) using npm install or yarn install.
  • Database Connection Issues: If you’re using a database, ensure your database connection details (host, username, password, database name) are correct. Also, handle potential database connection errors gracefully.
  • CORS Errors: If you’re making API requests from a different domain, you might encounter CORS (Cross-Origin Resource Sharing) errors. You can resolve this by configuring CORS in your API route or using a proxy. For local development, you might temporarily disable CORS in your browser (not recommended for production).
  • Incorrect Redirection: Make sure the getServerSideProps function in pages/[shortId].js correctly retrieves the longUrl from your database or data source. Also, verify that the redirection is working correctly.
  • Incorrect Environment Variables: If you’re using environment variables (e.g., for database credentials), make sure they are set correctly in your .env.local file and that you are accessing them correctly in your code (e.g., process.env.DATABASE_URL).
  • Not Handling Errors Properly: Implement proper error handling in both your client-side and server-side code. Display informative error messages to the user and log errors on the server for debugging.

Enhancements and Next Steps

This is a basic implementation, but there are several ways to enhance it:

  • Database Integration: The most important enhancement is to integrate a real database (e.g., MongoDB, PostgreSQL, MySQL) to store the long and short URLs persistently. This will allow you to track and manage URLs effectively.
  • Custom Short URLs: Allow users to specify a custom short URL instead of automatically generating one. This can be implemented by adding an extra input field in the form and modifying the API route to handle the custom short URL.
  • User Authentication: Implement user accounts to allow users to manage their shortened URLs, track clicks, and see analytics.
  • Analytics: Track the number of clicks on each shortened URL. You can store this information in your database and display it in a dashboard.
  • QR Code Generation: Generate QR codes for the shortened URLs.
  • Rate Limiting: Implement rate limiting to prevent abuse of your URL shortener service.
  • API Documentation: Create API documentation (e.g., using Swagger or OpenAPI) to make your API easier to use for others.
  • UI/UX Improvements: Improve the user interface and user experience, such as adding a copy-to-clipboard button for the shortened URL, providing visual feedback during the shortening process, and designing a more user-friendly interface.

Key Takeaways

  • Next.js Fundamentals: You’ve learned how to create pages, handle forms, make API requests, and use dynamic routes in Next.js.
  • API Routes: You’ve seen how to create API routes to handle backend logic.
  • State Management: You’ve used the useState hook to manage component state.
  • Server-Side Redirection: You’ve learned how to use getServerSideProps for server-side redirection.
  • URL Shortening Concepts: You’ve gained an understanding of how URL shorteners work.

Building a URL shortener app in Next.js is a fantastic way to learn about web development fundamentals. By starting with a simple project and then expanding its functionality, you can gain valuable experience with Next.js and related technologies. Remember to always prioritize security, especially when working with user input and data storage. Consider how you might scale this application to handle a large number of users and requests as you continue to develop it. As you explore the possibilities, you’ll be well on your way to building more complex and sophisticated web applications.