11
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

NextJS・NestJS・PostgreSQL・Dockerでフルスタックアプリを開発してみよう!ハンズオン①(サーバーサイド編)

Last updated at Posted at 2021-01-04

#はじめに
さて、人生初のブログを投稿しますw まず、この記事を書こうとしたきっかけについて説明しますね。

実は、以前から個人プロジェクトで近くの飲み友を探せるマッチングアプリ的なもの開発していました。元々は、React・GraphQL・PostgreSQLを使用して開発していたのですが、Typescriptを使用してNextJS・TYPEORM・GraphQL・PostgreSQLで改良しようと決意しました。

が、如何せんこれが難しくどこから手をつけるかわからず、、、正直心が折れそうになりました。トホホホ
特に、サーバーサイドで冗長化・可読性に長けたアーキテクチャーが見つからず、、、かといって絶対に良い方法があるはず!
(どこの企業もフリースタイルでサーバーサイドのコードを書いていないはず)
と諦めずに調査した結果この記事に辿り着きました。

Yahoo テックブログ

NestJS!? 聞いたことないフレームワーク!? しかも、NextJSと綴りほぼ同じだし笑

使ってみた結果、めっちゃくちゃいいやん!

ということで、Typescriptを使用したフルスタックのハンズオン的な記事があればいいなと思いこの記事を書くことにしました。 Yahooも使ってるし、最新の技術スタックのはず笑

ちなみに、今回はGraphQLではなくExpressを使用します。次回以降、GraphQLバージョンやKubernetesバージョンの記事も書きますね。

この記事がどなたかの役に立てれば幸いです。

コードだけをみたい方はこちら

前提条件

  • Dockerがインストールされていること。(Dockerhubをインストールした方が楽)
  • Nodeがインストールされていること(使用するパッケージの都合上、v12.0.0を推奨します。nvmをインストールしておくとバージョンの切り替えが便利です。)

対象読者

  • DockerやReactなど単独で勉強してきたが、どうやって一緒に使うか知りたい方
  • とりあえず、フルスタックのアプリをTypescriptだけで開発してみたい方
  • NextJS・NestJS聞いたことあるけれど、使ったことがない方・使ってみたい方

Goal

**I Theater(アイ・シアタ)**っというアプリを作ります。このブログのために考案しました。
ezgif.com-video-to-gif.gif
コロナ禍でみんな外出れないと思うので、後で見たい映画のタイトルをリスト化できるアプリです。

#本編
では、早速作っていきましょう!以下の構成でアプリを開発していきます。

##アプリの構成図

Architecture.png

  • ポイント①:nginxサーバーをブラウザとシステムの間に挟むことで、プロキシサーバー・ルーターとしての役割を与えたこと。
  • ポイント②:Dockerコンテナ内でアプリを起動。こうすることで、環境依存しない開発が可能であること。

サーバーサイド(NestJS)から書いていこう!

では、NestJSのCLIを使ってコードを書いていきましょう! ドキュメントはこちらを参照してください。

プロジェクト作成

まず、以下のCLIのコマンドを叩くことでベースとなるプロジェクトを作成しましょう!

npm i -g @nestjs/cli
nest new server 

すると、以下のようなフォルダ構成でプロジェクトが作成されます。
スクリーンショット 2021-01-04 19.37.12.png

AngularJSからきた人は馴染みがある構成かもしれません。

MovieListに必要なコードを作成しよう

今回、映画の名前を保存・映画の名前のリストを取得するためにMovieList関連のコードを作成する必要があります。

作成するファイルは合計で5つです。

  • movielist.entity.ts
  • movielist.interface.ts
  • movielist.controller.ts
  • movielist.service.ts
  • movielist.module.ts

movielist.entity.tsを作成

NestJSにはORMとしてTypeORMが内蔵されているので、じゃんじゃんその恩恵を受けましょう!

公式ドキュメントはこちら

まず、src下にmovielistフォルダを作り、movielist.entity.tsを作成します。

movielist.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { IsNotEmpty } from 'class-validator';

@Entity()
@Unique(['movieName'])
export class MovieList {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ nullable: false })
  @IsNotEmpty({ message: 'Movie Name must not be empty' })
  movieName: string;
}

カラムは自動生成されるidと映画の名前を保管するmovieNameにします。
movieNameカラムはNullチェック、Unique機能を付加することで、空文字を防止しておきましょう。

movielist.interface.tsを作成

今回、Typescriptを使用するということでデータの型を予め指定しておきましょう! 基本的に、movielist.entity.tsに合わせておきます。

nest g interface movielist/movielist 

上記のコマンドを叩くことで、movielist内にファイルが作成されます。
スクリーンショット 2021-01-04 19.37.12.png

インターフェースは以下で十分でしょう

movielist.interface.ts
export interface Movielist {
  id: number;
  movieName: string;
}

movielist.controller.tsを作成

次に、コントローラーファイルを作成しましょう!このファイルは司令塔のようなもので、nginxサーバからのリクエストとサービスクラスの間に位置するファイルです。

nest g controller movielist
movielist.controller.ts
import { Controller, Get, Post, Req } from '@nestjs/common';
import { MovielistService } from './movielist.service';

import { Request } from 'express';
import { Movielist } from './movielist.interface';

@Controller('movielist')
export class MovielistController {
  constructor(
    private movieListService: MovielistService,
  ) {}

  @Get()
  fetchAll(): Promise<Movielist[]> {
    return this.movieListService.fetchAll();
  }

  @Post()
  insertOne(@Req() request: Request): void {
    this.movieListService.insertOne(request.body.movieName);
  }
}

TYPEORMと同様、ExpressもNestJSに内蔵されています。通常のExpressと違い、GetとPostアノテーションをメソッドの前につけるだけでルートパスを指定することができるのが利点です。

NestJSにコントローラクラスであると伝達するために、クラス名の前にControllerアノテーションを付加します。今回はパラメータにmovielistを指定しているので、fetchAllメソッドを呼び出すためのパスはドメイン名/movielistです。

また、コンストラクタのパラメータ内に後ほど作るサービスを指定することで依存性を注入しておきます。

movielist.service.ts

では、サービスクラスを作っていきましょう! サービスクラスはデータベース処理が記述されているファイルです。

nest g service movielist

今回は映画のリストを全て取得するfetchAllと映画の名前を追加するinsertOneを実装します。データベースと連携させるために、typeormパッケージのRepository機能を利用します。

movielist.service.ts
import { Injectable, Param } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { MovieList } from './movielist.entity';
import { Movielist } from './movielist.interface';

@Injectable()
export class MovielistService {
  constructor(
    @InjectRepository(MovieList)
    private movieListRepository: Repository<MovieList>,
  ) {}

  async fetchAll(): Promise<Movielist[]> {
    return await this.movieListRepository.find();
  }

  async insertOne(@Param() movieName): Promise<void> {
    await this.movieListRepository.insert({
      movieName: movieName,
    });
  }
}

Injectableアノテーションをつけることでコントローラクラス側でDI(Dependency Injection)することが可能です。

movielist.module.tsを作成

最後に、MovieListの構成を設定するモジュールを作成します。以下のコマンドを叩きましょう。

nest g module movielist
movielist.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MovielistController } from './movielist.controller';
import { MovieList } from './movielist.entity';
import { MovielistService } from './movielist.service';

@Module({
  imports: [TypeOrmModule.forFeature([MovieList])],
  controllers: [MovielistController],
  providers: [MovielistService],
})
export class MovielistModule {}

このコードもTYPEORMを使用しているので、先ほど作成したエンティティをインポートしておきましょう!

AppモジュールにMovieListモジュールをインポートしよう!

NestJSプロジェクトは必ず1つルートであるモジュールが必要です。そのモジュールを起点に、子モジュールも含めてビルドします。

その前に必要なモジュールをインストールしておきましょう!

$ yarn add @nestjs/typeorm typeorm pg

app.module に先ほど作成したMovieListモジュールとormconfigファイルをインポートしておきましょう。

app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
import * as ormconfig from '../ormconfig';

import { MovielistModule } from './movielist/movielist.module';

@Module({
  imports: [TypeOrmModule.forRoot(ormconfig), MovielistModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {
  constructor(private connection: Connection) {}
}

TypeOrmModule.forRoot()はTYPEORMの設定に必要なプロパティに対応しています。パラメータとしてormconfigを指定します。

NestJSが起動するポートを5000に指定しておきましょう。

main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(5000);
}
bootstrap();

ormconfig.tsを作成

では最後に、ormconfigのrootフォルダに作成しておきましょう。

ormconfig.ts
import { ConnectionOptions } from 'typeorm';

// Check typeORM documentation for more information.
const config: ConnectionOptions = {
  type: 'postgres',
  host: process.env.PGHOST,
  port: parseInt(process.env.PGPORT) | 5432,
  username: process.env.PGUSER,
  password: process.env.PGPASSWORD,
  database: process.env.PGDATABASE,
  entities: [__dirname + '/**/*.entity{.ts,.js}'],
  // We are using migrations, synchronize should be set to false.
  synchronize: true,
  // Run migrations automatically,
  // You can disable this if you prefer running migration manually.
  migrationsRun: true,
  logging: true,
  logger: 'file',
  // Allow both start:prod and start:dev to use migrations
  // __dirname is either dist or src folder, meaning either
  // the compiled js in prod or the ts in dev
  migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
  cli: {
    migrationsDir: 'src/migrations',
  },
};

export = config;

synchronizeプロパティをtrueに設定しますが、本番運用の時はfalseにしておきましょう!基本的に、マイグレーションファイルで管理することが推奨されます。

終わりに

突然の終わりに驚きました?
すみません(;^ω^) 想定より長くなりようなので、前編と後編に分けることにしました。w

以上、今回はサーバーサイド編でした。

次の記事フロントエンド編に進んでください!

11
10
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
11
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?