Developing a Type-Safe URL Shortener: NestJS and TypeScript Guide
In this guide, you will develop a type-safe URL shortening service from scratch using the modular structure of NestJS.
You will learn step by step both the backend logic and the data validation process.
🧠 What Will You Learn in This Guide?
- Creating a new project with NestJS CLI,
- Establishing a database connection with TypeORM + SQLite,
- Generate unique short codes with
nanoid, - Data verification with
class-validator, - Applying the Controller and Service structure,
- You will learn to verify the abbreviation and routing logic with tests.
1️⃣ Preparing the Development Environment
Step 1.1 — NestJS CLI and Project Setup
Install NestJS CLI globally:
npm install -g @nestjs/cli
This command installs the NestJS command line tool on your system.
Create new project (example name: genixnode-link-shortcici):
nest new genixnode-link-kisaltici
cd genixnode-link-kisaltici
NestJS will create the necessary framework and select the package manager (npm).
Step 1.2 — Installing Required Dependencies
Install all required packages for database and validation:
npm install @nestjs/typeorm typeorm sqlite3
npm install class-validator class-transformer nanoid@^3.0.0
class-validator validates incoming data, nanoid generates unique shortcodes, and sqlite3 provides a simple local database.
Step 1.3 — Creating Module and Service Files
Create the base module, service and control files with the NestJS CLI:
nest generate module url
nest generate service url --no-spec
nest generate controller url --no-spec
These commands create url.module.ts, url.service.ts and url.controller.ts files respectively.
2️⃣ Database Connection and Entity Definition
Step 2.1 — Creating the URL Entity
nano src/url/url.entity.ts
Add the following code:
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Url {
@PrimaryGeneratedColumn()
id: number;
@Column()
urlCode: string; // nanoid tarafından üretilen benzersiz kod
@Column()
longUrl: string; // Orijinal uzun URL
@Column()
shortUrl: string; // Kısaltılmış URL
}
This model defines how URL data will be stored in the database.
Step 2.2 — Configuring Database Connection
nano src/app.module.ts
Add the following code:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Url } from './url/url.entity';
import { UrlModule } from './url/url.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'link_kisaltici.sqlite',
entities: [Url],
synchronize: true, // Geliştirmede aktif, üretimde kapalı olmalı
}),
UrlModule,
],
})
export class AppModule {}
synchronize: true automatically updates the database table. It should be false in the production environment.
Step 2.3 — Adding Repository Access to the Module
nano src/url/url.module.ts
import { Module } from '@nestjs/common';
import { UrlService } from './url.service';
import { UrlController } from './url.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Url } from './url.entity';
@Module({
imports: [TypeOrmModule.forFeature([Url])],
providers: [UrlService],
controllers: [UrlController],
})
export class UrlModule {}
The forFeature() method only accesses the Url store for this module.
3️⃣ Implementing Service Logic
Step 3.1 — Data Transfer Object (DTO) and Authentication
mkdir src/url/dtos
nano src/url/dtos/url.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';
export class ShortenURLDto {
@IsString()
@IsNotEmpty()
longUrl: string; // Zorunlu alan
}
Add global validation pipe in main application file:
nano 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({ whitelist: true }));
await app.listen(3000);
}
bootstrap();
This structure automatically checks that all incoming data complies with DTO rules.
Step 3.2 — Providing Repository Access to the Service
nano src/url/url.service.ts
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<Url>,
) {}
}
Thanks to InjectRepository, access to the TypeORM repository is provided.
Step 3.3 — Shortening Logic (shortenUrl)
async shortenUrl(url: ShortenURLDto): Promise<string> {
const { longUrl } = url;
if (!isURL(longUrl)) {
throw new BadRequestException('Lütfen geçerli bir URL girin');
}
let existing = await this.repo.findOneBy({ longUrl });
if (existing) return existing.shortUrl;
const urlCode = nanoid(10);
const baseURL = 'http://localhost:3000';
const shortUrl = `${baseURL}/${urlCode}`;
try {
const newUrl = this.repo.create({ urlCode, longUrl, shortUrl });
await this.repo.save(newUrl);
return newUrl.shortUrl;
} catch {
throw new UnprocessableEntityException('Sunucu hatası oluştu');
}
}
If the same URL is shortened again, the system improves efficiency by returning the previous record.
Step 3.4 — Redirect Logic (redirect)
async redirect(urlCode: string) {
const url = await this.repo.findOneBy({ urlCode });
if (url) return url;
throw new NotFoundException('Kaynak bulunamadı');
}
This method finds the shortcode and redirects to the original URL.
4️⃣ Defining Controller Routes
nano src/url/url.controller.ts
import {
Body,
Controller,
Get,
Param,
Post,
Res,
} from '@nestjs/common';
import { UrlService } from './url.service';
import { ShortenURLDto } from './dtos/url.dto';
import { Response } from 'express';
@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: Response, @Param('code') code: string) {
const urlEntity = await this.service.redirect(code);
return res.redirect(urlEntity.longUrl);
}
}
@Controller() is left without parameters and the root route (/) is listened to.
5️⃣ Testing
Start the application:
npm run start
6️⃣ URL Shortening Test
curl -X POST -H "Content-Type: application/json" \
-d '{"longUrl":"https://ornek.com/uzun-ornek-link"}' \
http://localhost:3000/shorten
This command shortens a long URL and returns the short link.
7️⃣ Routing Test
curl -v http://localhost:3000/sOmEkOd
-v parametresi yönlendirme detaylarını (302 yanıt kodu) gösterir.
❓ Frequently Asked Questions (FAQ)
- Can I use my own custom codes instead of nanoid?
Yes, but in this case you need to add the customCode field to the POST request and check for uniqueness in the database.
- Why is synchronize: true dangerous in production?
This setting synchronizes the table on every run, and incorrect changes may result in data loss.
- Why did we use
@Controller()instead of@Controller('url')?
Because shortened links should work as http://localhost:3000/kod; The url/code structure would be counterproductive.
- Why is using DTO important?
DTOs ensure the security of incoming data in terms of type and format. Erroneous or incomplete data is blocked before reaching the application.
🏁 Result
In this guide, you have developed a fully type-safe URL Shortener application using NestJS and TypeScript. You learned end-to-end data validation, service logic and routing processes.
💡 By distributing your project on GenixNode infrastructure, you can gain high scalability and security advantages. Try it now on the GenixNode platform.

