Skip to content

Commit 40505c5

Browse files
committed
redirect request with shorted url to original url
1 parent d44cc9b commit 40505c5

File tree

7 files changed

+39
-58
lines changed

7 files changed

+39
-58
lines changed

api/src/infrastructure/repository/in-memory-url.repository.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ export class InMemoryUrlRepository implements ShortenUrlRepository {
1010
return Promise.resolve(undefined);
1111
}
1212

13-
findShortenedURL(url: string): Promise<string | null> {
14-
const shortenedUrl = this.urls.get(url);
15-
return Promise.resolve(shortenedUrl);
16-
}
17-
1813
findOriginalURL(shortenedUrl: string): Promise<string | null> {
1914
const originalUrl = Array.from(this.urls.keys()).find(
2015
(key) => this.urls.get(key) === shortenedUrl,

api/src/infrastructure/repository/redis-url.repository.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ export class RedisUrlRepository
1414
{
1515
private readonly DEFAULT_TTL = 100;
1616

17-
private redisClient: RedisClientType;
17+
private readonly redisTTL: number;
1818

19-
private redisTTL: number;
19+
private redisClient: RedisClientType;
2020

2121
constructor(private readonly configService: ConfigService) {
2222
const redisUrl = this.configService.get<string>('REDIS_URL');
@@ -31,17 +31,6 @@ export class RedisUrlRepository
3131
}
3232

3333
async create(url: string, shortenedUrl: string): Promise<void> {
34-
// Use two keys to allow both forward and reverse lookup because:
35-
// Efficient retrieval (O(1) time complexity)
36-
// No need to scan all keys
37-
// Reduces query complexity
38-
await this.redisClient.set(
39-
`originalUrl:${url}`,
40-
`shortenedUrl: ${shortenedUrl}`,
41-
{
42-
EX: this.redisTTL,
43-
},
44-
); // 1 day expiry
4534
await this.redisClient.set(
4635
`shortenedUrl: ${shortenedUrl}`,
4736
`originalUrl: ${url}`,
@@ -51,10 +40,6 @@ export class RedisUrlRepository
5140
);
5241
}
5342

54-
async findShortenedURL(shortenedUrl: string): Promise<string | null> {
55-
return await this.redisClient.get(`shortenedUrl:${shortenedUrl}`);
56-
}
57-
5843
async findOriginalURL(originalUrl: string): Promise<string | null> {
5944
return await this.redisClient.get(`originalUrl:${originalUrl}`);
6045
}

api/src/shorten-url/get-original-url-controller.spec.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('ShortenUrl controller', () => {
1717
underTest = app.get<GetOriginalUrlController>(GetOriginalUrlController);
1818
});
1919

20-
describe('getOriginalUrl', () => {
20+
describe('Redirect to original url', () => {
2121
it('should return the original URL for a given shortened URL', async () => {
2222
const longUrl =
2323
'https://zapper.xyz/very-long-url/very-long-url/very-long-url';
@@ -26,15 +26,26 @@ describe('ShortenUrl controller', () => {
2626
const shortenedResponse = await shortenUrlController.shortenUrl({
2727
url: longUrl,
2828
});
29+
30+
// Extract the slug from the shortened URL
2931
const shortenedUrl = shortenedResponse.shortenedUrl;
32+
const slug = shortenedUrl.split('/').pop();
3033

3134
// Now retrieve the original URL using GetOriginalUrlController
32-
const response = await underTest.getOriginalUrl({
33-
shortenedUrl: shortenedUrl,
34-
});
35+
const response = await underTest.getOriginalUrl(slug);
3536

36-
expect(response).not.toBeNull();
37-
expect(response.originalUrl).toBe(longUrl);
37+
// TODO check with an integration / e2e test the redirection code (302)
38+
expect(response).toEqual({ url: longUrl });
3839
});
3940
});
41+
42+
it('should return 404 if the shortened URL is not found', async () => {
43+
const redirect = jest.fn();
44+
const res = { redirect } as any;
45+
46+
const slug = 'does-not-exist';
47+
await expect(underTest.getOriginalUrl(slug)).rejects.toThrow(
48+
'Shortened URL "does-not-exist" not found',
49+
);
50+
});
4051
});
Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1-
import { Body, Controller, Get } from '@nestjs/common';
2-
import { GetOriginalUrlDto } from './get-original-url.dto';
3-
import { ShortenUrlUsecase } from './shorten-url.usecase';
1+
import {
2+
Controller,
3+
Get,
4+
NotFoundException,
5+
Param,
6+
Redirect,
7+
} from '@nestjs/common';
48
import { GetOriginalUrlUsecase } from './get-original-url.usecase';
59

6-
@Controller('/shorten-url')
10+
@Controller()
711
export class GetOriginalUrlController {
812
constructor(private readonly getOriginalUrlUsecase: GetOriginalUrlUsecase) {}
913

10-
@Get()
11-
async getOriginalUrl(
12-
@Body() request: GetOriginalUrlDto,
13-
): Promise<{ originalUrl: string }> {
14-
const originalUrl = await this.getOriginalUrlUsecase.getOriginalUrl(
15-
request.shortenedUrl,
16-
);
17-
return { originalUrl };
14+
@Get(':slug')
15+
@Redirect(undefined, 302)
16+
async getOriginalUrl(@Param('slug') slug: string) {
17+
const originalUrl = await this.getOriginalUrlUsecase.getOriginalUrl(slug);
18+
if (!originalUrl) {
19+
throw new NotFoundException(`Shortened URL "${slug}" not found`);
20+
}
21+
// Use temporary redirect to original URL so that we can capture analytics
22+
return { url: originalUrl };
1823
}
1924
}

api/src/shorten-url/get-original-url.usecase.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export class GetOriginalUrlUsecase {
99
) {}
1010

1111
async getOriginalUrl(shortenedUrl: string): Promise<string | null> {
12-
return await this.shortenUrlRepository.findOriginalURL(shortenedUrl);
12+
// TODO: reconstruct the URL ? or store only the shorted path ??
13+
const url = 'http://localhost:3000/' + shortenedUrl;
14+
return await this.shortenUrlRepository.findOriginalURL(url);
1315
}
1416
}

api/src/shorten-url/shorten-url.repository.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
export abstract class ShortenUrlRepository {
2-
abstract findShortenedURL(url: string): Promise<string | null>;
3-
42
abstract create(url: string, shortenedUrl: string): Promise<void>;
53

64
abstract findOriginalURL(shortenedUrl: string): Promise<string | null>;

api/src/shorten-url/shorten-url.usecase.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,6 @@ export class ShortenUrlUsecase {
2727
}
2828

2929
async shortenUrl(originalURL: string): Promise<string> {
30-
const existingShortenedUrl =
31-
await this.shortenUrlRepository.findShortenedURL(originalURL);
32-
33-
if (existingShortenedUrl) {
34-
return existingShortenedUrl;
35-
}
3630
const id = this.idGenerator.generateId();
3731
const encodedId = this.encodeBase62(Number(id));
3832
const shortenedUrl = this.shortenedBaseUrl + '/' + encodedId;
@@ -51,13 +45,4 @@ export class ShortenUrlUsecase {
5145
}
5246
return encoded;
5347
}
54-
55-
private getBaseUrl(url: string): string {
56-
try {
57-
const parsedUrl = new URL(url);
58-
return `${parsedUrl.protocol}//${parsedUrl.hostname}`;
59-
} catch (error) {
60-
throw new Error(`Invalid URL: ${url}`);
61-
}
62-
}
6348
}

0 commit comments

Comments
 (0)