Build a Next.js Interactive Web-Based URL Shortener

Written by

in

In today’s digital landscape, links are everywhere. From social media posts to email campaigns, concise and manageable URLs are crucial for a better user experience and effective tracking. This is where URL shorteners come in. They take long, unwieldy web addresses and transform them into shorter, more user-friendly equivalents. In this tutorial, we’ll dive into building your own interactive web-based URL shortener using Next.js, a powerful React framework, and explore how to make it dynamic and functional.

Why Build Your Own URL Shortener?

While numerous URL shortening services are available, building your own offers several advantages:

  • Control: You have complete control over your data, privacy, and branding.
  • Customization: Tailor the service to your specific needs, such as adding analytics or custom domain support.
  • Learning: It’s a fantastic project to learn and practice web development skills, especially with a framework like Next.js.

This tutorial will guide you through the process, from setting up the project to deploying your URL shortener. You’ll learn about Next.js fundamentals, API routes, database interaction, and user interface design. Let’s get started!

Prerequisites

Before we begin, ensure you have the following:

  • Node.js and npm (or yarn): Installed on your machine.
  • Basic understanding of JavaScript and React: Familiarity with these technologies is helpful.
  • A code editor: Such as VS Code, Sublime Text, or Atom.

Project Setup

First, let’s create a new Next.js project using the command line. Open your terminal and run the following command:

npx create-next-app url-shortener

This command sets up a new Next.js project named “url-shortener”. Navigate into the project directory:

cd url-shortener

Now, install any necessary dependencies. For this project, we’ll use a library for generating unique short codes and a database client. We’ll use a popular database like MongoDB for this example, but you can choose any database you prefer. Install the following packages:

npm install nanoid mongodb

or if you are using yarn:

yarn add nanoid mongodb

Here’s a breakdown of the dependencies:

  • nanoid: For generating unique short codes.
  • mongodb: A MongoDB driver to communicate with a MongoDB database.

Database Setup (MongoDB Example)

If you don’t already have a MongoDB database, you can create a free account on MongoDB Atlas (https://www.mongodb.com/atlas/database). Once you have an account and have created a cluster, you’ll need to obtain your connection string. This string will be used to connect to your database from your Next.js application.

Important: Never expose your connection string directly in your client-side code. We’ll use environment variables to securely store and access the connection string.

Create a file named .env.local in the root directory of your project (if it doesn’t already exist) and add the following line, replacing <your_mongodb_connection_string> with your actual connection string:


MONGODB_URI=<your_mongodb_connection_string>

Your .env.local file should remain private and not be committed to your version control system (like Git). This prevents sensitive information from being exposed.

Creating API Routes

Next.js simplifies creating API routes. These routes handle the logic for shortening URLs and retrieving the original URLs. Create a new directory named pages/api in your project. Inside this directory, create a file named shorten.js. This file will handle the URL shortening process. Add the following code:


import { nanoid } from 'nanoid';
import { MongoClient } from 'mongodb';

const MONGODB_URI = process.env.MONGODB_URI;
const DB_NAME = 'url-shortener'; // Replace with your database name

async function connectToDatabase() {
  const client = new MongoClient(MONGODB_URI);
  try {
    await client.connect();
    console.log('Connected to MongoDB');
    return client.db(DB_NAME);
  } catch (error) {
    console.error('Error connecting to MongoDB:', error);
    throw error; // Re-throw the error to be handled by the caller
  }
}

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

    if (!originalUrl) {
      return res.status(400).json({ error: 'Original URL is required' });
    }

    try {
      const db = await connectToDatabase();
      const collection = db.collection('urls');
      const shortCode = nanoid(6); // Generate a short code

      // Check if the URL already exists
      const existingUrl = await collection.findOne({ originalUrl });
      if (existingUrl) {
        return res.status(200).json({ shortCode: existingUrl.shortCode, originalUrl: existingUrl.originalUrl });
      }

      const result = await collection.insertOne({
        originalUrl,
        shortCode,
        createdAt: new Date(),
        visits: 0,
      });

      res.status(201).json({ shortCode, originalUrl });
    } catch (error) {
      console.error('Error shortening URL:', error);
      res.status(500).json({ error: 'Failed to shorten URL' });
    } finally {
    }
  } else {
    res.status(405).json({ error: 'Method not allowed' });
  }
}

Let’s break down this code:

  • Import Statements: Import necessary modules.
  • Environment Variables: Retrieve the MongoDB connection string from your .env.local file.
  • connectToDatabase Function: Establishes a connection to your MongoDB database. It’s wrapped in a try/catch block to handle potential connection errors.
  • Handler Function (req, res): This is the main function that handles incoming requests.
  • Method Check: Checks if the request method is POST.
  • Input Validation: Checks if an original URL is provided in the request body.
  • Database Interaction:
    • Connects to the database.
    • Generates a unique short code using nanoid.
    • Inserts the original URL and short code into the database.
  • Response: Sends a JSON response containing the short code and original URL.
  • Error Handling: Includes error handling for database connection and insertion errors.

Now, create another API route file named [shortCode].js inside the pages/api directory. This route will handle redirecting users to the original URL when they visit the shortened URL:


import { MongoClient } from 'mongodb';

const MONGODB_URI = process.env.MONGODB_URI;
const DB_NAME = 'url-shortener'; // Replace with your database name

async function connectToDatabase() {
  const client = new MongoClient(MONGODB_URI);
  try {
    await client.connect();
    return client.db(DB_NAME);
  } catch (error) {
    console.error('Error connecting to MongoDB:', error);
    throw error; // Re-throw the error
  }
}

export default async function handler(req, res) {
  const { shortCode } = req.query;

  if (!shortCode) {
    return res.status(400).json({ error: 'Short code is required' });
  }

  try {
    const db = await connectToDatabase();
    const collection = db.collection('urls');
    const result = await collection.findOneAndUpdate(
      { shortCode },
      { $inc: { visits: 1 } }
    );

    if (!result.value) {
      return res.status(404).json({ error: 'URL not found' });
    }
    const originalUrl = result.value.originalUrl;
    res.redirect(301, originalUrl);
  } catch (error) {
    console.error('Error redirecting:', error);
    res.status(500).json({ error: 'Failed to redirect' });
  }
}

Key aspects of this code:

  • Dynamic Route: [shortCode].js uses a dynamic route, capturing the short code from the URL.
  • Database Lookup: It queries the database for the short code.
  • Redirection: If the short code is found, it redirects the user to the original URL using res.redirect(301, originalUrl). The 301 status code indicates a permanent redirect.
  • Error Handling: Handles cases where the short code is not found or other errors occur.
  • Visit Counter: It increments the visit counter for each shortened URL.

Building the User Interface (UI)

Now, let’s create the UI for your URL shortener. Open the pages/index.js file (this is the main page of your Next.js application) and replace the existing content with the following:


import { useState } from 'react';

export default function Home() {
  const [originalUrl, setOriginalUrl] = useState('');
  const [shortenedUrl, setShortenedUrl] = useState('');
  const [error, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);

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

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

      if (!response.ok) {
        const errorData = await response.json();
        setError(errorData.error || 'Failed to shorten URL');
        throw new Error(errorData.error || 'Failed to shorten URL');
      }

      const data = await response.json();
      setShortenedUrl(`${window.location.origin}/${data.shortCode}`);
    } catch (error) {
      console.error('Error shortening URL:', error);
      setError(error.message || 'An unexpected error occurred.');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <h1>URL Shortener</h1>
      
         setOriginalUrl(e.target.value)}
          required
        />
        <button type="submit" disabled="{isLoading}">
          {isLoading ? 'Shortening...' : 'Shorten'}
        </button>
      
      {error && <p>{error}</p>}
      {shortenedUrl && (
        <div>
          <p>Shortened URL: <a href="{shortenedUrl}" target="_blank" rel="noopener noreferrer">{shortenedUrl}</a></p>
        </div>
      )}
      
        {
          `
          .container {
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 2rem;
            font-family: sans-serif;
          }

          h1 {
            margin-bottom: 1rem;
          }

          form {
            display: flex;
            flex-direction: column;
            width: 100%;
            max-width: 400px;
            margin-bottom: 1rem;
          }

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

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

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

          .error {
            color: red;
            margin-top: 0.75rem;
          }

          .shortened-url {
            margin-top: 1rem;
            padding: 1rem;
            border: 1px solid #ddd;
            border-radius: 4px;
            text-align: center;
          }
          `
        }
      
    </div>
  );
}

Let’s break down the UI code:

  • State Variables: Uses the useState hook to manage the original URL, the shortened URL, any potential errors, and a loading state.
  • handleSubmit Function:
    • Prevents the default form submission behavior.
    • Resets the error and shortened URL.
    • Sets the loading state to true.
    • Sends a POST request to the /api/shorten API route with the original URL in the request body.
    • Handles the response, updating the shortened URL or displaying an error message.
    • Sets the loading state back to false in the finally block.
  • JSX Structure:
    • Contains a form with an input field for the original URL and a submit button.
    • Displays any error messages.
    • Displays the shortened URL, if it’s available, as a clickable link.
  • Styling: Includes basic inline styles for a clean and functional UI. You can customize these styles further to match your desired design.

Running the Application

To run your application, execute the following command in your terminal:

npm run dev

or

yarn dev

This will start the development server, and you can access your URL shortener in your web browser at http://localhost:3000. Enter a long URL, click “Shorten”, and you should get a shortened URL back!

Adding Features and Enhancements

This is a basic implementation. Here are some ideas for enhancements:

  • Custom Domains: Allow users to use their own domains. This involves DNS configuration and more complex routing.
  • Analytics: Track the number of clicks, geographic location, and other metrics for each shortened URL.
  • User Accounts: Implement user accounts to save shortened URLs and manage them.
  • QR Code Generation: Generate QR codes for the shortened URLs.
  • Rate Limiting: Prevent abuse by limiting the number of requests from a single IP address.
  • API Key Authentication: Add API key authentication if you plan to expose the shortening functionality to other applications.
  • URL Validation: Validate the input URL to ensure it is a valid URL format.

Common Mistakes and Troubleshooting

Here are some common issues and how to resolve them:

  • Database Connection Errors: Double-check your MongoDB connection string in your .env.local file and ensure your database server is running. Verify that your IP address is whitelisted in your MongoDB Atlas account if you are using Atlas.
  • CORS Errors: If you encounter CORS (Cross-Origin Resource Sharing) errors, make sure your API routes are correctly configured to handle requests from your frontend. You might need to install and configure a CORS middleware in your API route.
  • Incorrect URL Format: The input URL must be a valid URL (e.g., “https://www.example.com”). Implement client-side validation to prevent invalid URLs from being submitted.
  • 404 Errors: If you get a 404 error when trying to access a shortened URL, ensure that your [shortCode].js API route is correctly set up and that the short code exists in your database. Check the server logs for any errors.
  • Missing Dependencies: Make sure you have installed all the required dependencies (e.g., nanoid, mongodb).
  • Environment Variable Issues: Ensure your environment variables are correctly loaded. During development, you may need to restart the development server after changing the .env.local file.

Key Takeaways

  • Next.js provides a streamlined way to build web applications.
  • API routes in Next.js make it easy to create backend functionality.
  • Using environment variables is crucial for securing sensitive information.
  • Database interaction is essential for storing and retrieving data.
  • User interface design is key to a good user experience.

FAQ

Q: Can I use a different database?

A: Yes, you can. The example uses MongoDB, but you can adapt the database connection and query logic to work with other databases like PostgreSQL, MySQL, or SQLite. You will need to install the appropriate driver for your chosen database and modify the code accordingly.

Q: How do I deploy this application?

A: You can deploy your Next.js application to various platforms, such as Vercel (which is recommended, as Next.js is optimized for Vercel), Netlify, or AWS. You’ll need to configure your deployment environment to handle environment variables and database connections.

Q: How can I improve the UI?

A: You can use a CSS framework like Tailwind CSS, Bootstrap, or Material UI to create a more visually appealing and user-friendly interface. You can also add features like a copy-to-clipboard button for the shortened URL and a history of shortened URLs.

Q: How can I handle URL validation?

A: You can use a library like validator or the built-in URL constructor in JavaScript to validate the input URL on the client-side before sending it to the server. This prevents invalid URLs from being processed and improves the user experience. You should also validate the URL on the server-side for security reasons.

By following this tutorial, you’ve taken the first steps towards building your own URL shortener. You’ve learned about Next.js, API routes, database interaction, and UI design. From here, you can continue to expand and refine your project by adding more features and customizing the user experience. This project serves as a solid foundation for further exploration into web development, and it demonstrates how Next.js can be employed to build practical, interactive web applications. Building your own URL shortener offers a unique opportunity to enhance your development skills and create a valuable tool tailored to your specific needs.