DEV Community

Cover image for Build a Short Link Service Using Nest.js
Leapcell
Leapcell

Posted on

Build a Short Link Service Using Nest.js

What is a Short Link?

When you surf the internet, you've probably often seen links like t.cn/A67x8Y or bit.ly/3z4aBc. They are usually quite short, and clicking on them redirects you to a page with a completely different domain name than the original one.

These links are short links.

Why Are Short Links Needed?

Short links came into being during the early days of Twitter when there were strict character limits. At that time, their main purpose was to save characters and prevent links from taking up valuable space.

As short links became more widespread, the "shortness" of the link itself became less important. More uses were developed:

  • Data Analytics: Short link services often provide click statistics, making it easy for users to understand the number of clicks and the link's dissemination effectiveness.
  • Bypass Blocking: Some software, like Safari and certain email clients, is very strict about link tracking and will automatically clean tracking parameters from links. Using a short link can avoid this issue.
  • Aesthetics: Short links are clean and concise, making them suitable for sharing in plain text scenarios like social media and text messages, which simplifies formatting.

How Short Links Work

The short link process is divided into two steps:

Creating a Short Link:

  1. A user submits a long URL to our service.
  2. Our service generates a unique identifier for it (e.g., aK8xLq).
  3. The service stores the mapping between this "code" and the original long URL in a database.
  4. The identifier is returned to the user.

Redirecting to the Original Link from a Short Link:

  1. When someone clicks https://short.url/aK8xLq, the browser sends a request to our server.
  2. The server parses the identifier aK8xLq from the URL path.
  3. The server queries the database for this identifier to find the corresponding original long URL.
  4. The server returns a redirect status code (301/302) to the browser and includes the original long URL in the Location field of the response header.
  5. Upon receiving this response, the browser automatically redirects to the long URL specified in the Location field.

How the Identifier is Generated

Hash algorithms are commonly used for generation.

  1. Generate a Hash: Use the long URL itself, or the long URL plus a "salt" (a random string), as input for a hash algorithm like MD5 or SHA1 to generate a digest.
  2. Truncate a Segment: The previous step generates a string. We can take a segment of it (e.g., the first 6 characters) as the short code.
  3. Handle Collisions: Two different long URLs might generate the same short code. Before saving a short code to the database, we must check if it already exists. If it does, we can take a different segment or regenerate the short code.

Building Your Own Short Link Service

A short link service consists of two main functional modules:

  • Nest.js
  • PostgreSQL, as the database

1. Initialize the Project

Install the Nest.js CLI:

npm i -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

Create a new project using the CLI:

nest new url-shortener
Enter fullscreen mode Exit fullscreen mode

This will create a new folder named url-shortener and install all the necessary dependencies. Open this directory in your favorite editor to start coding.

2. Connect to the PostgreSQL Database

Next, we will integrate a PostgreSQL database. Following the official recommendation, we'll use TypeORM as the ORM. An ORM's role is to integrate the database into our code.

Install Dependencies

npm install @nestjs/typeorm typeorm pg # For PostgreSQL integration
npm install @nestjs/config class-validator class-transformer # For interface parameter validation
Enter fullscreen mode Exit fullscreen mode

Set up a Database

To simplify the steps, we won't install and build a database locally. Instead, we'll request an online one.

We can get a free database with one click on Leapcell.

Leapcell

After registering for an account on the website, click "Create Database".

ImageP1

Enter a Database name and select a deployment region to create the PostgreSQL database.

On the new page, you will see the information required to connect to the database. At the bottom, a control panel allows you to read and modify the database directly on the webpage.

ImageP2

Configure the Database Connection

Open the src/app.module.ts file and import TypeOrmModule.

Fill in the connection information using the database credentials you obtained from Leapcell. Note that ssl must be set to true, otherwise the connection will fail.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'your_postgres_host',
      port: 5432,
      username: 'your_postgres_username', // Replace with your PostgreSQL username
      password: 'your_postgres_password', // Replace with your PostgreSQL password
      database: 'your_postgres_db', // Replace with your database name
      schema: 'myschema',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true, // Set to true in development; it automatically syncs the DB structure
      ssl: true, // Required for services like Leapcell
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Create the Short Link Module

Next, create a module to manage short links.

You can quickly generate the necessary files using the Nest CLI:

nest generate resource short-link
Enter fullscreen mode Exit fullscreen mode

Afterward, we need to create the ShortLink entity file to connect it with the database. Create a file named short-link.entity.ts in the src/short-link directory:

// src/short-link/short-link.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, Index } from 'typeorm';

@Entity()
export class ShortLink {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  @Index() // Create an index on shortCode to speed up queries
  shortCode: string;

  @Column({ type: 'text' })
  longUrl: string;

  @CreateDateColumn()
  createdAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

Next, create a DTO (Data Transfer Object). The DTO is used to validate incoming request data, ensuring we receive a URL in a valid format.

// src/short-link/dto/create-short-link.dto.ts
import { IsUrl } from 'class-validator';

export class CreateShortLinkDto {
  @IsUrl({}, { message: 'Please provide a valid URL.' })
  url: string;
}
Enter fullscreen mode Exit fullscreen mode

Now we need to connect to the database.

Register TypeOrmModule in ShortLinkModule: open src/short-link/short-link.module.ts and import TypeOrmModule.forFeature([ShortLink]).

// src/short-link/short-link.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ShortLinkController } from './short-link.controller';
import { ShortLinkService } from './short-link.service';
import { ShortLink } from './short-link.entity';

@Module({
  imports: [TypeOrmModule.forFeature([ShortLink])],
  controllers: [ShortLinkController],
  providers: [ShortLinkService],
})
export class ShortLinkModule {}
Enter fullscreen mode Exit fullscreen mode

Go to the database details page on Leapcell and execute the following command in the web editor to generate the corresponding table:

CREATE TABLE "short_link" (
    "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    "shortCode" VARCHAR(255) NOT NULL UNIQUE,
    "longUrl" TEXT NOT NULL,
    "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX "IDX_short_code" ON "short_link" ("shortCode");
Enter fullscreen mode Exit fullscreen mode

Create the Short Link Service

The ShortLinkService will be responsible for handling all business logic related to short links. Open src/short-link/short-link.service.ts and add the following code:

// src/short-link/short-link.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ShortLink } from './short-link.entity'; // Corrected import path
import * as crypto from 'crypto';

@Injectable()
export class ShortLinkService {
  constructor(
    @InjectRepository(ShortLink)
    private readonly shortLinkRepository: Repository<ShortLink>
  ) {}

  // Find a link by its shortCode
  async findOneByCode(shortCode: string): Promise<ShortLink | null> {
    return this.shortLinkRepository.findOneBy({ shortCode });
  }

  // Create a new short link
  async create(longUrl: string): Promise<ShortLink> {
    // Check if the long link already exists; if so, return it to avoid duplicates
    const existingLink = await this.shortLinkRepository.findOneBy({ longUrl });
    if (existingLink) {
      return existingLink;
    }

    // Generate a unique shortCode
    const shortCode = await this.generateUniqueShortCode(longUrl);

    // Save to the database
    const newLink = this.shortLinkRepository.create({
      longUrl,
      shortCode,
    });

    return this.shortLinkRepository.save(newLink);
  }

  /**
   * Generate a hashed short code and handle collisions
   * @param longUrl The original URL
   */
  private async generateUniqueShortCode(longUrl: string): Promise<string> {
    const HASH_LENGTH = 7; // Define our desired short code length
    let attempt = 0; // Attempt counter

    // Set a maximum number of attempts to prevent an infinite loop
    while (attempt < 10) {
      // Salted hash: add the attempt number as a "salt" to ensure a different hash each time
      const salt = attempt > 0 ? String(attempt) : '';
      const hash = crypto
        .createHash('sha256')
        .update(longUrl + salt)
        .digest('base64url') // Use base64url for URL-safe characters
        .substring(0, HASH_LENGTH);

      const linkExists = await this.findOneByCode(hash);

      if (!linkExists) {
        // If the hash code doesn't exist in the database, it's unique and can be returned
        return hash;
      }

      // If it exists (a collision occurred), increment the attempt counter, and the loop will continue
      attempt++;
    }

    // If a unique code cannot be found after multiple attempts, throw an error
    throw new InternalServerErrorException(
      'Could not generate a unique short link. Please try again.'
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Create the Short Link Controller

The controller is responsible for handling HTTP requests, calling the service, and returning a response. Open src/short-link/short-link.controller.ts and add the following code:

// src/short-link/short-link.controller.ts
import { Controller, Get, Post, Body, Param, Res, NotFoundException } from '@nestjs/common';
import { Response } from 'express';
import { ShortLinkService } from './short-link.service';
import { CreateShortLinkDto } from './dto/create-short-link.dto';

@Controller()
export class ShortLinkController {
  constructor(
    private readonly shortLinkService: ShortLinkService,
  ) {}

  @Post('shorten')
  async createShortLink(@Body() createShortLinkDto: CreateShortLinkDto) {
    const link = await this.shortLinkService.create(createShortLinkDto.url);

    return {
      shortCode: link.shortCode,
    };
  }

  @Get(':shortCode')
  async redirect(@Param('shortCode') shortCode: string, @Res() res: Response) {
    const link = await this.shortLinkService.findOneByCode(shortCode);

    if (!link) {
      throw new NotFoundException('Short link not found.');
    }

    // Perform the redirect
    return res.redirect(301, link.longUrl);
  }
}
Enter fullscreen mode Exit fullscreen mode

Start the Project

Enable DTO validation in src/main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe()); // Add this line
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Run the following command in the terminal to start the project:

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Execute the following command in your console to create a short link:

curl -X POST http://localhost:3000/shorten \
-H "Content-Type: application/json" \
-d '{"url": "https://www.google.com/search?q=nestjs+url+shortener"}'

# Response: {"shortCode":"some-hash"}
Enter fullscreen mode Exit fullscreen mode

Access the short link:

Use the shortCode returned in the previous step to construct the full URL, http://localhost:3000/some-hash, and open it in your browser. You will be automatically redirected to the Google search page.

You can continue to enhance this short link service, for example, by logging the number of visits, visitors' IP addresses, etc.

Deploying the Short Link Service Online

Now you might be thinking, how can I deploy this service online so my short links can be shared on the internet?

Remember Leapcell, which we used to create the database? Leapcell can do more than just create databases; it is also a web app hosting platform that can host projects in various languages and frameworks, including Nest.js, of course.

Leapcell

Follow the steps below:

  1. Push your project to GitHub. You can refer to GitHub's official documentation for the steps. Leapcell will pull the code from your GitHub repository later.
  2. Click "Create Service" on the Leapcell page. ImageP3
  3. After choosing your Nest.js repo, you'll see Leapcell has auto-populated the necessary configurations. ImageP4
  4. Click "Submit" at the bottom to deploy. The deployment will complete quickly and return you to the deployment homepage. Here, we can see that Leapcell provides a domain. This is the online address for your short link service. ImageP5

Now, your short link service is live, and anyone can access your short links on the internet.


Follow us on X: @LeapcellHQ


Read on our blog

Related Posts:

Top comments (0)