#はじめに
さて、人生初のブログを投稿しますw まず、この記事を書こうとしたきっかけについて説明しますね。
実は、以前から個人プロジェクトで近くの飲み友を探せるマッチングアプリ的なもの開発していました。元々は、React・GraphQL・PostgreSQLを使用して開発していたのですが、Typescriptを使用してNextJS・TYPEORM・GraphQL・PostgreSQLで改良しようと決意しました。
が、如何せんこれが難しくどこから手をつけるかわからず、、、正直心が折れそうになりました。トホホホ
特に、サーバーサイドで冗長化・可読性に長けたアーキテクチャーが見つからず、、、かといって絶対に良い方法があるはず!
(どこの企業もフリースタイルでサーバーサイドのコードを書いていないはず)
と諦めずに調査した結果この記事に辿り着きました。
NestJS!? 聞いたことないフレームワーク!? しかも、NextJSと綴りほぼ同じだし笑
使ってみた結果、めっちゃくちゃいいやん!
ということで、Typescriptを使用したフルスタックのハンズオン的な記事があればいいなと思いこの記事を書くことにしました。 Yahooも使ってるし、最新の技術スタックのはず笑
ちなみに、今回はGraphQLではなくExpressを使用します。次回以降、GraphQLバージョンやKubernetesバージョンの記事も書きますね。
この記事がどなたかの役に立てれば幸いです。
コードだけをみたい方はこちら
前提条件
- Dockerがインストールされていること。(Dockerhubをインストールした方が楽)
- Nodeがインストールされていること(使用するパッケージの都合上、v12.0.0を推奨します。nvmをインストールしておくとバージョンの切り替えが便利です。)
対象読者
- DockerやReactなど単独で勉強してきたが、どうやって一緒に使うか知りたい方
- とりあえず、フルスタックのアプリをTypescriptだけで開発してみたい方
- NextJS・NestJS聞いたことあるけれど、使ったことがない方・使ってみたい方
Goal
**I Theater(アイ・シアタ)**っというアプリを作ります。このブログのために考案しました。
コロナ禍でみんな外出れないと思うので、後で見たい映画のタイトルをリスト化できるアプリです。
#本編
では、早速作っていきましょう!以下の構成でアプリを開発していきます。
##アプリの構成図
- ポイント①:nginxサーバーをブラウザとシステムの間に挟むことで、プロキシサーバー・ルーターとしての役割を与えたこと。
- ポイント②:Dockerコンテナ内でアプリを起動。こうすることで、環境依存しない開発が可能であること。
サーバーサイド(NestJS)から書いていこう!
では、NestJSのCLIを使ってコードを書いていきましょう! ドキュメントはこちらを参照してください。
プロジェクト作成
まず、以下のCLIのコマンドを叩くことでベースとなるプロジェクトを作成しましょう!
npm i -g @nestjs/cli
nest new server
すると、以下のようなフォルダ構成でプロジェクトが作成されます。
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
を作成します。
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内にファイルが作成されます。
インターフェースは以下で十分でしょう
export interface Movielist {
id: number;
movieName: string;
}
movielist.controller.tsを作成
次に、コントローラーファイルを作成しましょう!このファイルは司令塔のようなもので、nginxサーバからのリクエストとサービスクラスの間に位置するファイルです。
nest g controller movielist
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
機能を利用します。
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
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ファイルをインポートしておきましょう。
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に指定しておきましょう。
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フォルダに作成しておきましょう。
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
以上、今回はサーバーサイド編でした。
次の記事フロントエンド編に進んでください!