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:
nest new URL-shortener
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:
cd url-shortener
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:
npm install sqlite3
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:
npm install nanoid@^3.0.0
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:
nest generate module url
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:
mkdir src/url/dtos
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:
nano src/main.ts
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:
npm run start
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.