Compare commits

..

No commits in common. "0e742570ab22fbd1508dcc1930fd02d25892895e" and "af903ef4308908b83fb60f2a98ee2ce0c3c82c19" have entirely different histories.

15 changed files with 100 additions and 294 deletions

View file

@ -1 +0,0 @@
node_modules

View file

@ -1,2 +0,0 @@
PORT=3000
UI_URL=http://localhost:3001

3
.gitignore vendored
View file

@ -33,6 +33,3 @@ lerna-debug.log*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# ENV
.env

View file

@ -1,16 +0,0 @@
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ARG SERVER_PORT
ENV PORT $SERVER_PORT
EXPOSE $SERVER_PORT
CMD ["npm", "run", "start:dev"]

114
README.md
View file

@ -1,65 +1,73 @@
# Tiny URL Microservice
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
The Tiny URL Microservice is a simple URL shortening service built with Nest.js. It allows users to create shortened versions of long URLs, making them easier to share. This README provides an overview of the project, how to run it, and details about available routes.
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
## Table of Contents
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
- [Project Overview](#project-overview)
- [Prerequisites](#prerequisites)
- [Getting Started](#getting-started)
- [Using Docker Compose](#using-docker-compose)
- [Using .env File](#using-env-file)
- [API Routes](#api-routes)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)
## Description
## Project Overview
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
This project is a simple URL shortening microservice built with Nest.js. It provides the following features:
- URL Shortening: Convert long URLs into short, easy-to-share versions.
- URL Expansion: Expand shortened URLs to their original long form And redirect users to the original URL.
## Prerequisites
Before you begin, ensure you have met the following requirements:
- Node.js installed on your machine.
- Docker and Docker Compose (if you prefer to use Docker).
- `.env` file (if not using Docker) with the necessary environment variables. You can use .env.exmaple as a reference.
## Getting Started
To get the project up and running, follow the instructions below.
### Using Docker Compose
1. Clone the repository:
## Installation
```bash
git clone https://github.com/your-username/tiny-url-microservice.git
cd tiny-url-microservice
docker-compose -f docker-compose.yaml up -d --build
$ npm install
```
Your Nest.js server should now be running on port as like the port from the .env file.
## API Routes
# Shorten URL:
- Endpoint: POST /url-shortener/shorten
- Request Body: { "url": "https://example.com" }
- Response: { "data": "abc123", "status": 201 }
Expand URL:
Endpoint: GET /url-shortener/:shortUrl
Response: Redirects to the original long URL or redirect to the client UI 404 page if not found.
## Testing
To run the tests, run the following command:
## Running the app
```bash
npm run test
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

View file

@ -1,12 +0,0 @@
version: "3.8"
services:
nest-app:
build:
context: .
dockerfile: Dockerfile
args:
SERVER_PORT: ${SERVER_PORT}
ports:
- "${SERVER_PORT}:${SERVER_PORT}"
env_file:
- .env

135
package-lock.json generated
View file

@ -10,15 +10,11 @@
"license": "UNLICENSED",
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.2.0",
"dotenv": "^16.3.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"shortid": "^2.2.16",
"swagger-ui-express": "^5.0.0"
"shortid": "^2.2.16"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
@ -1687,32 +1683,6 @@
}
}
},
"node_modules/@nestjs/config": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.1.1.tgz",
"integrity": "sha512-qu5QlNiJdqQtOsnB6lx4JCXPQ96jkKUsOGd+JXfXwqJqZcOSAq6heNFg0opW4pq4J/VZoNwoo87TNnx9wthnqQ==",
"dependencies": {
"dotenv": "16.3.1",
"dotenv-expand": "10.0.0",
"lodash": "4.17.21",
"uuid": "9.0.0"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
"reflect-metadata": "^0.1.13"
}
},
"node_modules/@nestjs/config/node_modules/dotenv": {
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/motdotla/dotenv?sponsor=1"
}
},
"node_modules/@nestjs/core": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz",
@ -1750,25 +1720,6 @@
}
}
},
"node_modules/@nestjs/mapped-types": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.4.tgz",
"integrity": "sha512-xl+gUSp0B+ln1VSNoUftlglk8dfpUes3DHGxKZ5knuBxS5g2H/8p9/DSBOYWUfO5f4u9s6ffBPZ71WO+tbe5SA==",
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
"class-transformer": "^0.4.0 || ^0.5.0",
"class-validator": "^0.13.0 || ^0.14.0",
"reflect-metadata": "^0.1.12"
},
"peerDependenciesMeta": {
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/platform-express": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.0.tgz",
@ -1805,37 +1756,6 @@
"typescript": ">=4.8.2"
}
},
"node_modules/@nestjs/swagger": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.2.0.tgz",
"integrity": "sha512-W7WPq561/79w27ZEgViXS7c5hqPwT7QXhsLsSeu2jeBROUhMM825QKDFKbMmtb643IW5dznJ4PjherlZZgtMvg==",
"dependencies": {
"@nestjs/mapped-types": "2.0.4",
"js-yaml": "4.1.0",
"lodash": "4.17.21",
"path-to-regexp": "3.2.0",
"swagger-ui-dist": "5.11.0"
},
"peerDependencies": {
"@fastify/static": "^6.0.0",
"@nestjs/common": "^9.0.0 || ^10.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0",
"class-transformer": "*",
"class-validator": "*",
"reflect-metadata": "^0.1.12"
},
"peerDependenciesMeta": {
"@fastify/static": {
"optional": true
},
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/testing": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.0.tgz",
@ -2785,7 +2705,8 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/array-flatten": {
"version": "1.1.1",
@ -3769,25 +3690,6 @@
"node": ">=6.0.0"
}
},
"node_modules/dotenv": {
"version": "16.3.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz",
"integrity": "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/motdotla/dotenv?sponsor=1"
}
},
"node_modules/dotenv-expand": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
"integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
"engines": {
"node": ">=12"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -5962,6 +5864,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@ -6108,7 +6011,8 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
@ -7833,25 +7737,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.11.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.0.tgz",
"integrity": "sha512-j0PIATqQSEFGOLmiJOJZj1X1Jt6bFIur3JpY7+ghliUnfZs0fpWDdHEkn9q7QUlBtKbkn6TepvSxTqnE8l3s0A=="
},
"node_modules/swagger-ui-express": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz",
"integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==",
"dependencies": {
"swagger-ui-dist": ">=5.0.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
@ -8382,14 +8267,6 @@
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",

View file

@ -21,15 +21,11 @@
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.2.0",
"dotenv": "^16.3.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"shortid": "^2.2.16",
"swagger-ui-express": "^5.0.0"
"shortid": "^2.2.16"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",

View file

@ -2,14 +2,9 @@ import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { UrlShortenerModule } from './url-shortener/url-shortener.module';
import { DnsService } from './dns/dns.service';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Makes the config globally available
}),UrlShortenerModule,
],
imports: [UrlShortenerModule],
controllers: [AppController],
providers: [DnsService],
})

View file

@ -19,6 +19,10 @@ describe('DnsService', () => {
dnsLookupMock = (dns.lookup as unknown) as jest.Mock;
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should return true for a valid URL', async () => {
dnsLookupMock.mockImplementation((hostname, callback) => callback(null, '1.1.1.1'));
expect(await service.isValidUrl('http://example.com')).toBe(true);

View file

@ -1,16 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as dotenv from 'dotenv';
import { setupSwagger } from '../swagger';
dotenv.config();
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = process.env.SERVER_PORT || 3004;
app.enableCors();
setupSwagger(app);
await app.listen(port);
console.log(`Application is running on: ${await app.getUrl()}`);
await app.listen(3000);
}
bootstrap();

View file

@ -1,5 +0,0 @@
export interface ApiResponse {
data?: string;
error?: string;
status: number;
}

View file

@ -1,37 +1,26 @@
import { Controller, Post, Body, Get, Param, HttpException, HttpStatus, Res } from '@nestjs/common';
import { Controller, Post, Body, Get, Param, Redirect, HttpException, HttpStatus } from '@nestjs/common';
import { UrlShortenerService } from './url-shortener.service';
import { ApiResponse } from './response.interface';
import { Response } from 'express';
import { ConfigService } from '@nestjs/config';
@Controller('url-shortener')
export class UrlShortenerController {
constructor(private readonly urlShortenerService: UrlShortenerService, private configService: ConfigService) {}
constructor(private readonly urlShortenerService: UrlShortenerService) {}
@Post('shorten')
async shortenUrl(@Body('url') url: string): Promise<ApiResponse> {
async shortenUrl(@Body('url') url: string): Promise<{ shortUrl: string }> {
if (!url) {
throw new HttpException('URL is required', HttpStatus.BAD_REQUEST);
}
try {
const shortUrl = await this.urlShortenerService.generateShortUrl(url);
return { data: shortUrl, status: HttpStatus.CREATED };
} catch (error) {
return { error: error.message, status: HttpStatus.BAD_REQUEST };
}
return { shortUrl };
}
@Get(':shortUrl')
async resolveShortUrl(@Param('shortUrl') shortUrl: string, @Res() res: Response) {
try {
@Redirect()
async resolveShortUrl(@Param('shortUrl') shortUrl: string) {
const originalUrl = this.urlShortenerService.getOriginalUrl(shortUrl);
if (!originalUrl) {
return res.redirect(`${this.configService.get<string>('UI_URL')}/404`);
}
return res.redirect(originalUrl);
} catch (error) {
throw new HttpException('An error occurred', HttpStatus.INTERNAL_SERVER_ERROR);
throw new HttpException('URL not found!', HttpStatus.NOT_FOUND);
}
return { url: originalUrl };
}
}

View file

@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import * as shortid from 'shortid';
import { DnsService } from '../dns/dns.service';
import { HttpException, HttpStatus } from '@nestjs/common';
@Injectable()
@ -13,20 +12,18 @@ export class UrlShortenerService {
async generateShortUrl(originalUrl: string): Promise<string> {
if(!originalUrl) {
throw new HttpException('URL is required', HttpStatus.BAD_REQUEST);
throw new Error('URL is required');
}
const isValid = await this.dnsService.isValidUrl(originalUrl);
let isValid = await this.dnsService.isValidUrl(originalUrl);
if(!isValid) {
throw new HttpException('URL is invalid', HttpStatus.BAD_REQUEST);
throw new Error('URL is invalid');
}
const shortUrl = "tiny." + shortid.generate() + ".com";
let shortUrl = shortid.generate();
this.urlMap.set(shortUrl, originalUrl);
return shortUrl;
return shortUrl
}
getOriginalUrl(shortUrl: string): string | undefined {
let url: string | undefined = this.urlMap.get(shortUrl);
if (url) return url;
return undefined;
return this.urlMap.get(shortUrl);
}
}

View file

@ -1,13 +0,0 @@
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { INestApplication } from '@nestjs/common';
export function setupSwagger(app: INestApplication): void {
const options = new DocumentBuilder()
.setTitle('Your API Title')
.setDescription('API description')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document);
}