0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Nest.js 短縮URLサービスにクリック計測を追加する

Last updated at Posted at 2025-09-17

Cover

前の記事で、基本的な短縮URLサービスを構築しました。

包括的な短縮URLサービスとなるためには、データ分析機能を追加することが不可欠です。データ分析は、短縮URLサービスのコアバリューの1つです。リンクのクリックを追跡することで、その普及効果、ユーザープロファイル、その他の情報を理解することができます。

次は、サービスにクリック計測機能を追加し、クリックごとに時間、IPアドレス、デバイス情報を記録します。

1. クリックレコード用のデータベースエンティティを作成する

まず、各クリックレコードを保存するための新しいデータベーステーブルが必要です。同様に、TypeORMエンティティを作成してその構造を定義します。

src ディレクトリに click という名前の新しいフォルダを作成し、その中に click.entity.ts ファイルを作成します。

// src/click/click.entity.ts

import { ShortLink } from '../short-link/short-link.entity';
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  ManyToOne,
  JoinColumn,
} from 'typeorm';

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

  // ShortLink エンティティとの多対一の関係を確立
  // これは、1つの短縮URLが多数のクリックレコードに対応できることを意味します
  @ManyToOne(() => ShortLink, (shortLink) => shortLink.clicks)
  @JoinColumn({ name: 'shortLinkId' }) // データベースに外部キー列を作成します
  shortLink: ShortLink;

  @CreateDateColumn()
  clickedAt: Date;

  @Column({ type: 'varchar', length: 45 }) // IPv4またはIPv6アドレスを格納するため
  ipAddress: string;

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

説明:

  • @ManyToOne: このデコレーターは、ClickShortLink の間の関係を確立します。1つの短縮URLには、多数(Many)のクリックが関連付けることができます。
  • @JoinColumn: データベースレベルで2つのテーブルを関連付けるために使用される外部キー列の名前を指定します。

2. ShortLink エンティティを更新する

短縮URLからすべてのクリックレコードをクエリできるように、short-link.entity.ts ファイルも更新して双方向の関係を確立する必要があります。

src/short-link/short-link.entity.ts を開き、clicks プロパティを追加します。

// src/short-link/short-link.entity.ts
import { Click } from '../click/click.entity'; // Click エンティティをインポート
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  Index,
  OneToMany,
} from 'typeorm';

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

  // ... その他の既存フィールド ...
  @Column({ unique: true })
  @Index()
  shortCode: string;

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

  @CreateDateColumn()
  createdAt: Date;

  // 新しいプロパティ
  @OneToMany(() => Click, (click) => click.shortLink)
  clicks: Click[];
}

説明:

  • @OneToMany: ShortLink から Click への一対多の関係を確立します。1つの短縮URL(One)は、多数(Many)のクリックレコードを持つことができます。

3. クリックサービス用のモジュールとロジックを作成する

ShortLink と同様に、Click 用の専用モジュールとサービスを作成し、関連するロジックを処理し、コードのモジュール性を維持します。

Nest CLI を使用して、必要なファイルを迅速に生成します。

nest generate resource click

ClickModule を構成する

src/click/click.module.ts を開き、TypeOrmModule をインポートし、Click エンティティを登録します。

// src/click/click.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ClickService } from './click.service';
import { Click } from './click.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Click])],
  providers: [ClickService],
  exports: [ClickService], // ClickService をエクスポートして、他のモジュールで使用できるようにします
})
export class ClickModule {}

ClickService を実装する

src/click/click.service.ts を開き、クリックを記録するためのロジックを記述します。

// src/click/click.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Click } from './click.entity';
import { ShortLink } from '../short-link/short-link.entity';

@Injectable()
export class ClickService {
  constructor(
    @InjectRepository(Click)
    private readonly clickRepository: Repository<Click>
  ) {}

  async recordClick(shortLink: ShortLink, ipAddress: string, userAgent: string): Promise<Click> {
    const newClick = this.clickRepository.create({
      shortLink,
      ipAddress,
      userAgent,
    });

    return this.clickRepository.save(newClick);
  }
}

4. リダイレクト中のクリック計測を統合する

次に、ShortLinkControllerredirect メソッドに追跡ロジックを統合します。

ShortLinkModule を更新する

まず、ShortLinkModuleClickModule をインポートして ClickService を使用できるようにする必要があります。

// 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';
import { ClickModule } from '../click/click.module'; // ClickModule をインポート

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

ShortLinkController を変更する

src/short-link/short-link.controller.ts を開き、ClickService を注入し、redirect メソッドでそれを呼び出します。

// src/short-link/short-link.controller.ts
import { Controller, Get, Post, Body, Param, Res, NotFoundException, Req } from '@nestjs/common';
import { Response, Request } from 'express';
import { ShortLinkService } from './short-link.service';
import { CreateShortLinkDto } from './dto/create-short-link.dto';
import { ClickService } from '../click/click.service'; // ClickService をインポート

@Controller()
export class ShortLinkController {
  constructor(
    private readonly shortLinkService: ShortLinkService,
    private readonly clickService: ClickService // ClickService を注入
  ) {}

  // ... createShortLink メソッドは変更されません ...
  @Post('shorten')
  // ...
  @Get(':shortCode')
  async redirect(
    @Param('shortCode') shortCode: string,
    @Res() res: Response,
    @Req() req: Request // Express Request オブジェクトを注入
  ) {
    const link = await this.shortLinkService.findOneByCode(shortCode);

    if (!link) {
      throw new NotFoundException('Short link not found.');
    }

    // ユーザーのリダイレクトを遅延させないように、クリックの記録を非同期で行います
    this.clickService.recordClick(link, req.ip, req.get('user-agent') || '');

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

主な変更点:

  1. ClickService を注入しました。
  2. redirect メソッドで、@Req() デコレーターを介して expressRequest オブジェクトを取得しました。
  3. req オブジェクトから、req.ip(IPアドレス)と req.get('user-agent')(デバイスとブラウザの情報)を取得しました。
  4. this.clickService.recordClick() を呼び出して、クリックレコードを保存しました。注意、クリック記録操作がリダイレクトをブロックしないように、ここでは await を使用しませんでした。

5. 統計情報を表示する

データが記録されたら、それを見るためのエンドポイントも必要です。ShortLinkController に統計エンドポイントを追加しましょう。

// src/short-link/short-link.controller.ts
// ... (インポートとコンストラクタ)

@Controller()
export class ShortLinkController {
  // ... (コンストラクタとその他のメソッド)

  // 新しい統計エンドポイント
  @Get('stats/:shortCode')
  async getStats(@Param('shortCode') shortCode: string) {
    const linkWithClicks = await this.shortLinkService.findOneByCodeWithClicks(shortCode);

    if (!linkWithClicks) {
      throw new NotFoundException('Short link not found.');
    }

    return {
      shortCode: linkWithClicks.shortCode,
      longUrl: linkWithClicks.longUrl,
      totalClicks: linkWithClicks.clicks.length,
      clicks: linkWithClicks.clicks.map((click) => ({
        clickedAt: click.clickedAt,
        ipAddress: click.ipAddress,
        userAgent: click.userAgent,
      })),
    };
  }
}

このエンドポイントが機能するためには、ShortLinkServicefindOneByCodeWithClicks メソッドを追加する必要があります。

src/short-link/short-link.service.ts を開きます。

// src/short-link/short-link.service.ts
// ...

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

  // ... (findOneByCode および create メソッド)

  // 関連を使用して関連するクリックをロードする新しいメソッド
  async findOneByCodeWithClicks(shortCode: string): Promise<ShortLink | null> {
    return this.shortLinkRepository.findOne({
      where: { shortCode },
      relations: ['clicks'], // キー: TypeORM に関連するクリックエンティティをロードするように指示します
    });
  }
}

これでサービスを再起動します。短縮URLにアクセスした後、http://localhost:3000/stats/your-short-codeGET リクエストを行うと、以下のようなJSONレスポンスが表示され、詳細なクリックレコードが含まれています。

{
  "shortCode": "some-hash",
  "longUrl": "https://www.google.com/",
  "totalClicks": 1,
  "clicks": [
    {
      "clickedAt": "2025-09-17T23:00:00.123Z",
      "ipAddress": "::1",
      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..."
    }
  ]
}

これで、短縮URLサービスにはクリック計測と統計分析機能が備わりました!

本番環境への更新

サービスがすでにオンラインで実行されている場合、ローカルでデバッグした後の次のステップは、更新を本番サーバーにデプロイすることです。

Leapcell にデプロイされたアプリケーションの場合、この手順は非常に簡単です。GitHub にコードをプッシュするだけで、Leapcell が最新のコードを自動的にプルしてアプリケーションを更新します。

Leapcell

さらに、Leapcell 自体には強力な組み込みtraffic analysis機能があります。クリック計測を自分で実装しなくても、Leapcell ダッシュボードに表示されるデータから深いユーザーインサイトを得ることができます。

ImageP1


Xでフォローする:@LeapcellJP


ブログでこの記事を読む

関連記事:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?