Understanding URL and Implementing a URL Shortener with NestJS

URL, an abbreviation for Uniform Resource Locator, is an address given to a unique resource on the web. Because a URL is unique, no two resources can have the same URL.

The length and complexity of URLs vary. A URL might be as short as example.com or as lengthy as http://llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.co.uk. Complex URLs can be unsightly, cause search engine optimization (SEO) issues, and negatively impact marketing plans. URL shorteners map a long URL to a shorter URL and redirect the user to the original URL when the short URL is used.

Introduction

In this tutorial, you will create a URL shortener using NestJS. First, you will implement the URL shortening and redirecting logic in a service. Then, you will create route handlers to facilitate the shortening and redirection requests.

Prerequisites

To follow this tutorial, you will need:

  • A local development environment for Node.js version 16 or higher. Follow the How To Install Node.js and Create a Local Development Environment tutorial that suits your system.
  • The NestJS CLI installed on your system, which you will set up in Step 1, and familiarity with NestJS. Review Getting Started with NestJS.
  • Familiarity with TypeScript. You can review the How To Code in TypeScript series to learn more.

Step 1 — Preparing Your Development Environment

In this step, you will set up everything you need to start implementing your URL shortening logic. You will install NestJS globally, generate a new NestJS application boilerplate, install dependencies, and create your project’s module, service, and controller.

First, you will install the Nest CLI globally if you have not previously installed it. You will use this CLI to generate your project directory and the required files. Run the following command to install the Nest CLI:

 
npm install -g @nestjs/cli 

The -g flag will install the Nest CLI globally on your system.

You will see the following output:

 
...
added 249 packages, and audited 250 packages in 3m
39 packages are looking for funding
run npm fund for details
found 0 vulnerabilities 

Then you will use the new command to create the project and generate the necessary boilerplate starter files:

You will see the following output:

 
...
⚡  We will scaffold your app in a few seconds..

CREATE url-shortener/.eslintrc.js (631 bytes)
CREATE url-shortener/.prettierrc (51 bytes)
CREATE url-shortener/nest-cli.json (118 bytes)
CREATE url-shortener/package.json (2002 bytes)
CREATE url-shortener/README.md (3339 bytes)
CREATE url-shortener/tsconfig.build.json (97 bytes)
CREATE url-shortener/tsconfig.json (546 bytes)
CREATE url-shortener/src/app.controller.spec.ts (617 bytes)
CREATE url-shortener/src/app.controller.ts (274 bytes)
CREATE url-shortener/src/app.module.ts (249 bytes)
CREATE url-shortener/src/app.service.ts (142 bytes)
CREATE url-shortener/src/main.ts (208 bytes)
CREATE url-shortener/test/app.e2e-spec.ts (630 bytes)
CREATE url-shortener/test/jest-e2e.json (183 bytes)

Move to your created project directory:

You will run all subsequent commands in this directory.

Note: The NestJS CLI creates app.controller.ts, app.controller.spec.ts, and app.service.ts files when you generate a new project. Because you won’t need them in this tutorial, you can either delete or ignore them.

Installing Required Dependencies for Your URL Shortener

Next, you will install the required dependencies. This tutorial requires a few dependencies, which you will install using NodeJS’s default package manager npm. The required dependencies include TypeORM, SQLite, Class-validator, Class-transformer, and Nano-ID.

Run the command to install TypeORM and its dedicated NestJS package:

 
npm install @nestjs/typeorm typeorm 

The following command is for installing SQLite:

Run the following command to install class-validator:

 
npm install class-validator 

Run the following command to install class-transformer:

 
npm install class-transformer 

The following command is for installing Nano-ID:

After you have installed the required dependencies, you will generate the project’s module, service, and controller using the Nest CLI. The module will organize your project, the service will handle all the logic for the URL shortener, and the controller will handle the routes.

Run the following command to generate your module:

You will see the following output:

 
CREATE src/url/url.module.ts (80 bytes)
UPDATE src/app.module.ts (304 bytes)

Next, run the following command to generate your service:

 
nest generate service url --no-spec 

The --no-spec flag tells the Nest CLI to generate the files without their test files. You won’t need the test files in this tutorial.

You will see the following output:

 
CREATE src/url/url.service.ts (87 bytes)
UPDATE src/url/url.module.ts (151 bytes)

Then run the following command to generate your controller:

 
nest generate controller url --no-spec 

You will see the following output:

 
CREATE src/url/url.controller.ts (95 bytes)
UPDATE src/url/url.module.ts (233 bytes)

In this step, you generated your application and most of the files needed for your development. Next, you’ll connect your application to a database.

Step 2 — Connecting Your Application to a Database

In this step, you’ll create an entity to model the URL resource in your database. An entity is a file containing the necessary properties of the stored data. You will also create a repository as an access layer between your application and its database.

Using nano or your preferred text editor, create and open a file in the src/url folder called url.entity.ts:

 
nano src/url/url.entity.ts 

This file will contain the entity to model your data.

Next, in your src/url/url.entity.ts file, add the following TypeScript code:

 
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Url {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    urlCode: string;

    @Column()
    longUrl: string;

    @Column()
    shortUrl: string;
}

First, you import the Entity, Column, and PrimaryGeneratedColumn decorators from 'typeorm'.

The code creates and exports a class Url annotated with the Entity decorator that marks a class as an entity.

Save and close the file.

Step 3 — Implementing Service Logic

In this step, you will implement your service logic with two methods. The first method, shortenUrl, will contain all the URL shortening logic. The second method, redirect, will contain all the logic to redirect a user to the original URL.

Open src/url/url.service.ts:

 
nano src/url/url.service.ts 

Add the following highlighted lines:

 
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Url } from './url.entity';

@Injectable()
export class UrlService {
  constructor(
    @InjectRepository(Url)
    private repo: Repository,
  ) {}
}

Your service now has access to your repository through the repo variable. All the database queries and TypeORM methods will be called on it.

Next, you will create an asynchronous method, shortenUrl. The method will take a URL as an argument and return a shortened URL. To validate that the data fed into the method is valid, you’ll use a data-transfer object together with the class-validator and class-transformer packages to validate the data.

Creating a Data-Transfer Object

First, create a dtos (data-transfer objects) folder within your url folder:

Then, create a file named url.dto.ts within that folder:

 
nano src/url/dtos/url.dto.ts 

Add the following code to the new file:

 
import { IsString, IsNotEmpty } from 'class-validator';

export class ShortenURLDto {
  @IsString()
  @IsNotEmpty()
  longUrl: string;
}

You import the IsString and IsNotEmpty decorators from class-validator. Then, you create and export the class ShortenURLDto. Inside your ShortenURLDto class, you create a longUrl property and assign it a type of string.

Save and close the file.

Next, open your src/main.ts file:

Add the highlighted pieces of code to the existing file:

 
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({ whitelist: true }));
  await app.listen(3000);
}
bootstrap();

You import ValidationPipe, which uses the class-validator package to enforce validation rules on all data coming into your application.

Save and close the file.

Creating the shortenUrl Method

First, open your src/url/url.service.ts file:

 
nano src/url/url.service.ts 

Add the highlighted lines to the file:

 
import {
  BadRequestException,
  Injectable,
  NotFoundException,
  UnprocessableEntityException,
} from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Url } from './url.entity';
import { ShortenURLDto } from './dtos/url.dto';
import { nanoid } from 'nanoid';
import { isURL } from 'class-validator';

@Injectable()
export class UrlService {
  constructor(
    @InjectRepository(Url)
    private repo: Repository,
  ) {}

  async shortenUrl(url: ShortenURLDto) {
    const { longUrl } = url;

    // Checks if longUrl is a valid URL
    if (!isURL(longUrl)) {
      throw new BadRequestException('String Must be a Valid URL');
    }

    const urlCode = nanoid(10);
    const baseURL = 'http://localhost:3000';

    try {
      // Check if the URL has already been shortened
      let url = await this.repo.findOneBy({ longUrl });
      // Return it if it exists
      if (url) return url.shortUrl;

      // If it doesn't exist, shorten it
      const shortUrl = `${baseURL}/${urlCode}`;

      // Add the new record to the database
      url = this.repo.create({
        urlCode,
        longUrl,
        shortUrl,
      });

      this.repo.save(url);
      return url.shortUrl;
    } catch (error) {
      console.log(error);
      throw new UnprocessableEntityException('Server Error');
    }
  }
}

This is what your url.service.ts file will now look like. Save and close the file.

Creating the redirect Method

The redirect method will contain the logic that redirects users to the long URL.

Still in the src/url/url.service.ts file, add the following code to the bottom of your UrlService class to implement the redirect method:

 
async redirect(urlCode: string) {
    try {
        const url = await this.repo.findOneBy({ urlCode });
        if (url) return url;
    } catch (error) {
        console.log(error);
        throw new NotFoundException('Resource Not Found');
    }
}

The redirect method takes urlCode as an argument and tries to find a resource in the database with a matching urlCode. If the resource exists, it will return the resource. Otherwise, it throws a NotFoundException error.

Your completed url.service.ts file will now look like this:

 
import {
  BadRequestException,
  Injectable,
  NotFoundException,
  UnprocessableEntityException,
} from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Url } from './url.entity';
import { ShortenURLDto } from './dtos/url.dto';
import { nanoid } from 'nanoid';
import { isURL } from 'class-validator';

@Injectable()
export class UrlService {
  constructor(
    @InjectRepository(Url)
    private repo: Repository,
  ) {}

  async shortenUrl(url: ShortenURLDto) {
    const { longUrl } = url;

    if (!isURL(longUrl)) {
      throw new BadRequestException('String Must be a Valid URL');
    }

    const urlCode = nanoid(10);
    const baseURL = 'http://localhost:3000';

    try {
      let url = await this.repo.findOneBy({ longUrl });
      if (url) return url.shortUrl;

      const shortUrl = `${baseURL}/${urlCode}`;

      url = this.repo.create({
        urlCode,
        longUrl,
        shortUrl,
      });

      this.repo.save(url);
      return url.shortUrl;
    } catch (error) {
      console.log(error);
      throw new UnprocessableEntityException('Server Error');
    }
  }

  async redirect(urlCode: string) {
    try {
      const url = await this.repo.findOneBy({ urlCode });
      if (url) return url;
    } catch (error) {
      console.log(error);
      throw new NotFoundException('Resource Not Found');
    }
  }
}

Save and close the file.

Step 4 — Implementing Controller Logic

In this step, you’ll create two route handlers: a POST route handler to handle shortening requests and a GET route handler to handle redirection requests.

First, open your src/url/url.controller.ts file:

 
nano src/url/url.controller.ts 

Add the highlighted lines to the file:

 
import { Body, Controller, Get, Param, Post, Res } from '@nestjs/common';
import { UrlService } from './url.service';
import { ShortenURLDto } from './dtos/url.dto';

@Controller()
export class UrlController {
  constructor(private service: UrlService) {}

  @Post('shorten')
  shortenUrl(
    @Body()
    url: ShortenURLDto,
  ) {
    return this.service.shortenUrl(url);
  }

  @Get(':code')
  async redirect(
    @Res() res,
    @Param('code')
    code: string,
  ) {
    const url = await this.service.redirect(code);

    return res.redirect(url.longUrl);
  }
}

Save and close the file. Your controller is now fully set up to handle URL shortening and redirection.

Step 5 — Testing the URL Shortener

To test the URL shortener, start your application:

Open a new terminal and use curl or your preferred API testing tool to make a POST request to http://localhost:3000/shorten with the data below:

 
curl -d "{\"longUrl\":\"http://llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.co.uk\"}" -H "Content-Type: application/json" http://localhost:3000/shorten 

You will receive a short URL as a response. Copy the short URL, paste it into your browser, and press ENTER. You will be redirected to the original resource.

Conclusion

In this tutorial, you created a URL shortener with NestJS. NestJS provides type-safety and architecture to make your application more secure, maintainable, and scalable. To learn more, visit the NestJS official documentation.

Create a Free Account

Register now and get access to our Cloud Services.

Posts you might be interested in: