🔗

Nest.js を使ったショートリンクサービスの構築

に公開

Cover

ショートリンクとは?

インターネットをサーフィンしていると、t.cn/A67x8Ybit.ly/3z4aBc のようなリンクをよく見かけるでしょう。これらは通常非常に短く、クリックすると元のドメイン名とはまったく異なるドメイン名のページにリダイレクトされます。

これらのリンクがショートリンクです。

ショートリンクはなぜ必要か?

ショートリンクは、Twitter の初期の頃、文字数制限が厳しかった時代に登場しました。当時、その主な目的は文字数を節約し、リンクが貴重なスペースを占有しないようにすることでした。

ショートリンクが普及するにつれて、リンク自体の「短さ」はそれほど重要ではなくなりました。より多くの用途が開発されました。

  • データ分析:ショートリンクサービスはクリック統計を提供することが多く、ユーザーはクリック数やリンクの拡散効果を容易に理解できます。
  • ブロッキングの回避:Safari や一部のメールクライアントなど、一部のソフトウェアはリンク追跡に非常に厳しく、追跡パラメータをリンクから自動的にクリーニングします。ショートリンクを使用すると、この問題を回避できます。
  • 美観:ショートリンクはクリーンで簡潔であるため、ソーシャルメディアやテキストメッセージなどのプレーンテキストのシナリオでの共有に適しており、フォーマットを簡素化します。

ショートリンクの仕組み

ショートリンクのプロセスは 2 つのステップに分かれています。

ショートリンクの作成:

  1. ユーザーが長い URL をサービスに送信します。
  2. サービスがそれに一意の識別子(例:aK8xLq)を生成します。
  3. サービスは、この「コード」と元の長い URL のマッピングをデータベースに保存します。
  4. 識別子がユーザーに返されます。

ショートリンクから元のリンクへのリダイレクト:

  1. 誰かが https://short.url/aK8xLq をクリックすると、ブラウザがサーバーにリクエストを送信します。
  2. サーバーが URL パスから識別子 aK8xLq を解析します。
  3. サーバーがデータベースをクエリして、この識別子に対応する元の長い URL を見つけます。
  4. サーバーがブラウザにリダイレクトステータスコード(301/302)を返し、レスポンスヘッダーの Location フィールドに元の長い URL を含めます。
  5. このレスポンスを受信すると、ブラウザは Location フィールドに指定された長い URL に自動的にリダイレクトします。

識別子の生成方法

ハッシュアルゴリズムが一般的に生成に使用されます。

  1. ハッシュの生成:MD5 や SHA1 のようなハッシュアルゴリズムで、長い URL 自体、または長い URL に「ソルト」(ランダムな文字列)を追加したものを入力として、ダイジェストを生成します。
  2. セグメントの切り抜き:前のステップで文字列が生成されます。その一部(例:最初の 6 文字)をショートコードとして取得できます。
  3. 衝突の処理: 2 つの異なる長い URL が同じショートコードを生成する可能性があります。ショートコードをデータベースに保存する前に、それが既に存在するかどうかを確認する必要があります。存在する場合は、別のセグメントを使用するか、ショートコードを再生成できます。

独自のショートリンクサービスの構築

ショートリンクサービスは、 2 つの主要な機能モジュールで構成されています。

  • Nest.js
  • PostgreSQL、データベースとして

1. プロジェクトの初期化

Nest.js CLI をインストールします。

npm i -g @nestjs/cli

CLI を使用して新しいプロジェクトを作成します。

nest new url-shortener

これにより、url-shortener という名前の新しいフォルダが作成され、必要なすべての依存関係がインストールされます。コーディングを開始するために、このディレクトリをお気に入りのエディタで開きます。

2. PostgreSQL データベースへの接続

次に、PostgreSQL データベースを統合します。公式の推奨に従い、ORM として TypeORM を使用します。ORM の役割は、データベースをコードに統合することです。

依存関係のインストール

npm install @nestjs/typeorm typeorm pg # PostgreSQL 統合用
npm install @nestjs/config class-validator class-transformer # インターフェースパラメータ検証用

データベースの設定

手順を簡略化するため、データベースをローカルにインストールしてビルドしません。代わりに、オンラインのデータベースをリクエストします。

Leapcell でワンクリックで無料のデータベースを取得できます。

Leapcell

ウェブサイトでアカウントに登録した後、「データベースの作成」をクリックします。

ImageP1

データベース名を入力し、デプロイメントリージョンを選択して PostgreSQL データベースを作成します。

新しいページには、データベースに接続するために必要な情報が表示されます。下部には、ウェブページ上で直接データベースを読み取ったり変更したりできるコントロールパネルがあります。

ImageP2

データベース接続の設定

src/app.module.ts ファイルを開き、TypeOrmModule をインポートします。

Leapcell から取得したデータベース資格情報を使用して、接続情報を入力します。ssltrue に設定する必要があることに注意してください。そうしないと、接続が失敗します。

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'your_postgres_host',
      port: 5432,
      username: 'your_postgres_username', // PostgreSQL ユーザー名に置き換えてください
      password: 'your_postgres_password', // PostgreSQL パスワードに置き換えてください
      database: 'your_postgres_db', // データベース名に置き換えてください
      schema: 'myschema',
      entities: [__dirname + '/**/*.entity{.ts,.js}'] ,
      synchronize: true, // 開発中は true に設定してください。DB 構造を自動的に同期します。
      ssl: true, // Leapcell のようなサービスでは必須です
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

ショートリンクモジュールの作成

次に、ショートリンクを管理するためのモジュールを作成します。

Nest CLI を使用して、必要なファイルを素早く生成できます。

nest generate resource short-link

その後、データベースと接続するために ShortLink エンティティファイルを作成する必要があります。src/short-link ディレクトリに short-link.entity.ts という名前のファイルを作成します。

// src/short-link/short-link.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, Index } from 'typeorm';

@Entity()
export class ShortLink {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  @Index() // クエリを高速化するために shortCode にインデックスを作成
  shortCode: string;

  @Column({ type: 'text' })
  longUrl: string;

  @CreateDateColumn()
  createdAt: Date;
}

次に、DTO(Data Transfer Object)を作成します。DTO は incoming リクエストデータを検証するために使用され、有効な形式の URL を受信することを保証します。

// src/short-link/dto/create-short-link.dto.ts
import { IsUrl } from 'class-validator';

export class CreateShortLinkDto {
  @IsUrl({}, { message: '有効な URL を提供してください。' })
  url: string;
}

これで、データベースに接続する必要があります。

ShortLinkModuleTypeOrmModule を登録します。src/short-link/short-link.module.ts を開き、TypeOrmModule.forFeature([ShortLink]) をインポートします。

// src/short-link/short-link.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ShortLinkController } from './short-link.controller';
import { ShortLinkService } from './short-link.service';
import { ShortLink } from './short-link.entity';

@Module({
  imports: [TypeOrmModule.forFeature([ShortLink])],
  controllers: [ShortLinkController],
  providers: [ShortLinkService],
})
export class ShortLinkModule {}

Leapcell のデータベース詳細ページに移動し、Web エディタで次のコマンドを実行して対応するテーブルを生成します。

CREATE TABLE "short_link" (
    "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    "shortCode" VARCHAR(255) NOT NULL UNIQUE,
    "longUrl" TEXT NOT NULL,
    "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX "IDX_short_code" ON "short_link" ("shortCode");

ショートリンクサービスの作成

ShortLinkService は、ショートリンクに関連するすべてのビジネスロジックを処理する責任があります。src/short-link/short-link.service.ts を開き、次のコードを追加します。

// src/short-link/short-link.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ShortLink } from './short-link.entity'; // インポートパスを修正しました
import * as crypto from 'crypto';

@Injectable()
export class ShortLinkService {
  constructor(
    @InjectRepository(ShortLink)
    private readonly shortLinkRepository: Repository<ShortLink>,
  ) {}

  // shortCode でリンクを検索
  async findOneByCode(shortCode: string): Promise<ShortLink | null> {
    return this.shortLinkRepository.findOneBy({ shortCode });
  }

  // 新しいショートリンクを作成
  async create(longUrl: string): Promise<ShortLink> {
    // 長いリンクが既に存在するかどうかを確認します。存在する場合は、重複を避けるためにそれを返します。
    const existingLink = await this.shortLinkRepository.findOneBy({ longUrl });
    if (existingLink) {
      return existingLink;
    }

    // 一意の shortCode を生成します
    const shortCode = await this.generateUniqueShortCode(longUrl);

    // データベースに保存
    const newLink = this.shortLinkRepository.create({
      longUrl,
      shortCode,
    });

    return this.shortLinkRepository.save(newLink);
  }

  /**
   * ハッシュ化されたショートコードを生成し、衝突を処理します
   * @param longUrl 元の URL
   */
  private async generateUniqueShortCode(longUrl: string): Promise<string> {
    const HASH_LENGTH = 7; // 目的のショートコードの長さを定義
    let attempt = 0; // 試行回数カウンター

    // 無限ループを防ぐために、最大試行回数を設定します
    while (attempt < 10) {
      // ソルト済みハッシュ:試行回数を「ソルト」として追加し、毎回異なるハッシュを生成
      const salt = attempt > 0 ? String(attempt) : '';
      const hash = crypto
        .createHash('sha256')
        .update(longUrl + salt)
        .digest('base64url') // URL 安全な文字のために base64url を使用
        .substring(0, HASH_LENGTH);

      const linkExists = await this.findOneByCode(hash);

      if (!linkExists) {
        // ハッシュコードがデータベースに存在しない場合、それは一意であり、返すことができます
        return hash;
      }

      // 存在する場合(衝突が発生した場合)、試行回数カウンターを増やし、ループを継続します
      attempt++;
    }

    // 複数回の試行で一意のコードが見つからない場合は、エラーをスローします
    throw new InternalServerErrorException(
      '一意のショートリンクを生成できませんでした。もう一度お試しください。'
    );
  }
}

ショートリンクコントローラーの作成

コントローラーは、HTTP リクエストを処理し、サービスを呼び出し、レスポンスを返す責任があります。src/short-link/short-link.controller.ts を開き、次のコードを追加します。

// src/short-link/short-link.controller.ts
import { Controller, Get, Post, Body, Param, Res, NotFoundException } from '@nestjs/common';
import { Response } from 'express';
import { ShortLinkService } from './short-link.service';
import { CreateShortLinkDto } from './dto/create-short-link.dto';

@Controller()
export class ShortLinkController {
  constructor(
    private readonly shortLinkService: ShortLinkService,
  ) {}

  @Post('shorten')
  async createShortLink(@Body() createShortLinkDto: CreateShortLinkDto) {
    const link = await this.shortLinkService.create(createShortLinkDto.url);

    return {
      shortCode: link.shortCode,
    };
  }

  @Get(':shortCode')
  async redirect(@Param('shortCode') shortCode: string, @Res() res: Response) {
    const link = await this.shortLinkService.findOneByCode(shortCode);

    if (!link) {
      throw new NotFoundException('ショートリンクが見つかりませんでした。');
    }

    // リダイレクトを実行
    return res.redirect(301, link.longUrl);
  }
}

プロジェクトの開始

src/main.ts で DTO 検証を有効にします。

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()); // この行を追加
  await app.listen(3000);
}
bootstrap();

プロジェクトを開始するには、ターミナルで次のコマンドを実行します。

npm run start:dev

コンソールで次のコマンドを実行して、ショートリンクを作成します。

curl -X POST http://localhost:3000/shorten \
-H "Content-Type: application/json" \
-d '{"url": "https://www.google.com/search?q=nestjs+url+shortener"}'

# レスポンス: {"shortCode":"some-hash"}

ショートリンクにアクセスする:

前のステップで返された shortCode を使用して、完全な URL http://localhost:3000/some-hash を構築し、ブラウザで開きます。Google 検索ページに自動的にリダイレクトされます。

このショートリンクサービスを、訪問数、訪問者の IP アドレスなどを記録するなど、さらに強化し続けることができます。

ショートリンクサービスのオンラインデプロイ

ここで、このサービスをオンラインにデプロイして、ショートリンクをインターネット上で共有できるようにするにはどうすればよいのでしょうか?

データベースを作成するために使用した Leapcell を覚えていますか? Leapcell はデータベースを作成するだけでなく、Web アプリケーションホスティングプラットフォームでもあり、もちろん Nest.js を含むさまざまな言語やフレームワークのプロジェクトをホストできます。

Leapcell

以下の手順に従います。

  1. プロジェクトを GitHub にプッシュします。手順については、GitHub の公式ドキュメントを参照してください。Leapcell は後で GitHub リポジトリからコードをプルします。
  2. Leapcell ページで「サービスを作成」をクリックします。
    ImageP3
  3. Nest.js リポジトリを選択すると、Leapcell が必要な設定を自動的に入力します。
    ImageP4
  4. 下部にある「送信」をクリックしてデプロイします。デプロイはすぐに完了し、デプロイメントホームページに戻ります。ここで、Leapcell がドメインを提供していることがわかります。これはショートリンクサービスのオンラインアドレスです。
    ImageP5

これで、ショートリンクサービスがライブになり、誰でもインターネット上でショートリンクにアクセスできるようになります。


Xでフォローする:@LeapcellJP


ブログでこの記事を読む

関連記事:

Discussion