4
4

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.

Nest.jsとSapperを組みわせて快適プロトタイピング環境を作ってみた

Last updated at Posted at 2021-02-16

背景:

色々と巷で良いと言われているフロントエンドフレームワークを色々仕事で使った事があるのですが(Angular>React>Vue)、テストやプロトタイピング的な案件では、去年から全てSapperで作っています。

Svelteはシンプルで記述量も少なく、コードが読み易く再利用がしやすいのでとても気に入っていますが、バックエンド搭載でSSRにも対応しているSapperの方が便利なので普段はこちらを使います。

ただ、Sapperのバックエンドは癖が強く再利用がしにくいので、バックエンドに関しては、スピード感のある開発ができて本番運用もし易いNest.jsを使いたい。(結局は、好みなんですが)。

色々書いたのですが、主な要望は以下です。

  • 型に関しては共通で書きたい!(将来のために!)
  • RESTやDBなどの通信系が最初のウチは雑に書ける。(あまり時間を割きたくない)
  • 一度作ったものが他の箇所で使用し易い。
  • Front+Back系プロジェクト特有のゴチャつきを防ぎたい

説明中、用語の使い方や表現のゆらぎが気になる方は、下の記事を読むと多分頭が少しスッキリします。
お前らがModelと呼ぶアレをなんと呼ぶべきか。近辺の用語(EntityとかVOとかDTOとか)について整理しつつ考える

「リンク早よっ!」て方は、こちらから:github: Nest.js, Sapper with Clean Archtecture: Working Example

Framework構成

基本的に、nestをexpressで設定すると、sapperのミドルウェア使えるので設定しただけです。
秘密はそれだけ。

async function bootstrap() {
	const app = await NestFactory.create(ServerModule);

	app.setGlobalPrefix(endpoints.prefix); // 干渉を避けるためGlobalPrefix="api"
	app.use(
		compression({ threshold: 0 }),
		sirv('static', { dev }), // SSRの要?
		sapper.middleware({ignore: /^\/api(.*)/}), // # /apiから始まるpathには適応しない。
	);

	await app.listen(PORT || 3000);
}
bootstrap();

フォルダ構成

プロトタイピングとは言っても、案外MVPまでプロジェクトが進行してしまう事が良くあるので、ある程度本番でも、使える様にしておきます。

基本的には、"クリーンアーキテクチャの文脈"にそいつつ、使いやすさを考えながら組んでいます。ただ、*1ゴリゴリの実装は返って書く量が増えてしまうので、今回はみんな馴染みのあるMVCとDDDの風味を残しておきます。

*1. 実装クリーンアーキテクチャ
CleanArchitecture.jpg

/domain: Entity

早速です、DDDならDomain、クリーンアーキテクチャならEntityに当たります。
名前はまちまちですが、考え方的には、次のapplication(Use Case)層で扱うときに、下の図くらいドメインモデルが抽象化されていると理想です。


ref: 関心事に相応しい命名をする

どういう事か、
図で言う。予約品(aggregate)はデータベースやシステムを見ると、顧客情報と予約情報と商品情報の集合です。
ドメインのRepositoryでは予約品という、システムから見て抽象化された業務で使う単位で、保存、読み出しできる事が理想です。

なので、分け方としては、以下のようにします。

domain/repository/yoyaku-hin.repository.ts
domain/model/yoyaku-hin.model.ts
domain/entity/kokyaku.entity.ts
domain/entity/yoyaku.entity.ts
domain/entity/shouhin.entity.ts

このようにrepositoryがaggregateをしているため、ここでCRUDを書いておくと、最初はControllerに直結して使う事ができます。
ここで、以下の様に雑でも、汎用なスーパークラスを作っておくと、ミスが減る上に、かなり時間が節約。
DDDでは、yoyaku-hin.modelも「一意な識別子によって特定される」ため、本当はEntityなのですが、ややこしいので
O/Rマッピングの対象を.entity.tsに書き、ドメインモデルを.model.tsに書いています。

// #shared/pattern/repository-controller.class.ts
export class RepositoryController<T> implements IRepositoryProxy {
  constructor(
    private readonly repository: IReposiotry
  ) { }

  @Get()
  findAll(@Query() filter) {
    return this.repository.findAll(filter);
  }

  @Get(':id')
  findById(@Param('id') id, @Query() filter) {
    return this.repository.findAll(id, filter);
  }

  @Post()
  create(@Body() body, @AuthUser() user) {
    return this.repository.create(user, body);
  }
}

// #interface/controller/yoyaku.controller.ts
import { endpoints } from 'interface/api/api.conf';

@Controller(endpoints.paths.yoyaku)
@UseGuards(AuthGuard())
export class YoyakuController extends RepositoryController<YoyakuHin>{

  constructor(
    yoyakuRepo: YoyakuRepository
  ) {
    super(yoyakuRepo);
  }
  
}

若干バカバカしく見えるかも知れないけど、コイツが結構便利で、プロジェクトが進行をしていっても、
成長するに連れてブラッシュアップしていったり、いくつかバージョンを作ると最後まで生き残ったりします。
ただ基本的には、Mock向けで複数人いる場合とりあえず繋げて、フロントエンドを開発し易くする事ができます。

/application: Use Case

RepositoryとAuthGuardとSuperControlerのコンボは強力で、一見「もう、やる事ねーじゃん。Application層、イラネーじゃん」となりそうですが。ここからが、本番です。
例えば、単純に予約品を登録(POST)するにしても、実際にはデータの保存だけでなく、認証や権限を確認したり、お客さんにメールを送ったりと沢山する事があります。
ここでもやっぱり、IRepositoryProxyを実装したSuperServiceを基本的には継承し、必要な箇所を上書きしていくと、時間が短縮できます。

@Injectable()
export class YoyakuService extends RepositoryService<YoyakuHin> {

  constructor(
    private readonly yoyakuRepo: YoyakuRepository,
    private readonly smsService: SmsService,
  ) {
    super(yoyakuRepo);
  }

  // 例:オリジナルを上書きし、デフォルト値を設定する
  findAll(_filter): Promise<YoyakuHin[]> {
    const filter = Object.assign({default: false}, _filter);
    return this.yoyakuRepo.findAll(filter);
  }

  // 例:オリジナルを上書きし、保存後にSMSを送信する。
  async create(body, user): Promise<YoyakuHin> {
    const yoyakuHin = await this.yoyakuRepo.creat(body, user);
    this.smsService.sendSendSMS(yoyakuHin);
    return yoyakuHin;
  }
  
}

/interface: Controller, Adapter

ここでは、システム間のインターフェースを設定します。
サーバのControllerとクライアントのService(Angular的?)に当たりますが、名前がService被りしている上に、application/serviceにきちんとロジック書いているなら、コイツの役目は基本にサーバーに繋ぐだけ。それ以外に変なことはして欲しくないので、Adapterに名前変更。
AuthentecatedAdapterとPublicAdapterぽいスーパークラス作り、各pathを変数に入れることで、ほぼコピペで済み、ミスが格段に減ります。
あとは、ここにDev/Hom/Prodのホスト名や各pathを書いておくと超便利。

// #interface/api/api.conf.ts
export const endpoints = {
  prefix: `api`,
  paths: {
    yoyaku: `yoyaku-hin`
  }
}

// #interface/adapter/yoyaku.adapter.ts
import { endpoints } from 'interface/api/api.conf';

export class YoyakuHinAdapter extends AuthentecatedAdapter<YoyakuHin> {
  constructor(){
    super(endpoints.paths.yoyaku);
  }
}

// #interface/controller/yoyaku.controller.ts
import { endpoints } from 'interface/api/api.conf';

@Controller(endpoints.paths.yoyaku)
@UseGuards(AuthGuard())
export class YoyakuController extends RepositoryController<YoyakuHin>{

  constructor(yoyakuRepo: YoyakuRepository) {
    super(yoyakuRepo);
  }
  
}

/infrastructure: 設定・インフラ

このにはサーバーやデータベースの接続設定、データベースのコネクターは/domain/*.entity.tsを読み込んで管理する。
以下の様にすると、オニオンアーキテクチャの様にデータベースが、Entityに依存する仕様に作れる。

// #/infrastructure/sql.conn.ts
import { YoyakuEntiy, KokyakuEntity, ShouHinEntity } from 'domain/model';
import { connect } from 'some-db-connector'

export const connection = connect({
  models: [YoyakuEntiy, KokyakuEntity, ShouHinEntity]
});

/components: UI

Svelteのコンポーネントを記述し行きます。何のことはないですね、
componentsかuiか悩みましたが、馴染みがある方にしました。

/routes: Web

Sapperの構造上、コイツがウェブのルートに対応しています。これはもう好きに呼んで下さい。

試したい点

仕組みを調べる時間がなかったので、SSRの機構をそのままにしてあります。これをNestのモノに置き換えれるのか、また、パーフォーマンスが上がるのかが気になります。

やり残した事

プロトタイピングが主な使い方なので、ユニットテストの設定を移植してません(雑)。
storybookとか入れたらいい感じのボイラープレートになるかも知れんです。
後READMEとか、ちゃんと書きたいところです。

後書き

オニオンアーキテクチャで組んだのは、初めてなので、間違っていたり。改善できる点があれば、教えてくれるとありがたいです。

プロジェクト: github: Nest.js, Sapper with Clean Archtecture: Working Example

4
4
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
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?