初めまして、株式会社Another works CTOの塩原です。
弊社では、複業クラウドというサービスを運営しており、今回はそのサービスのAPI側の設計を公開したいと思います。
レイヤー
レイヤーは三層構造でdomainレイヤー、applicationレイヤー、othersレイヤーという風にしています。
それぞれ外側への依存を禁止するような構成にしています。
レイヤーの設計はこの記事を参考にしています。ref: https://nrslib.com/adop/#outline__3_3_2
依存関係
ディレクトリ構成
コーディング規約
typescriptを使っているので、typescriptのコーディング基準に則っています
https://typescript-jp.gitbook.io/deep-dive/styleguide#filename
命名規則
eslintで縛っています
'@typescript-eslint/naming-convention': [
"error",
{
"selector": "default",
"format": ["camelCase"]
},
{
"selector": "typeLike",
"format": ["PascalCase"]
},
{
"selector": "class",
"format": ["PascalCase"]
},
{
"selector": "interface",
"format": ["PascalCase"]
},
{
"selector": "typeAlias",
"format": ["PascalCase"]
},
{
"selector": "objectLiteralProperty",
"format": null
}
],
Type vs Enum
Typeを使用する
Enumは使用しない
Typeの書き方
export type CompanyStatus = 'init' | 'active'
// 値に名前を付けたいとき
// 外からつかうときにclientStatusValueをimportして使う
export const clientStatusValue = {
init: 0,
active: 1,
} as const;
export type ClientStatus = typeof clientStatusValue[keyof typeof clientStatusValue];
null vs undefined
nullも使用を許可する
DBにもnullが使われているため
namespaceを使うタイミング
exportが複数あるとき
namespaceの命名はファイル名にする
使用する
export namespace ModuleA {
export type TypeA = 'abc' | 'def'
export type TypeB = 'xyz' | '123'
}
使用しない
type TypeA = 'abc' | 'def'
export default TypeA
ディレクトリ構成
-- src
|-- api
|-- controller
|-- response
|-- route
|-- types
api
expressのコードが入る
apiに関するクラス
route
-- route
|-- public
|-- healthCheckPublicRoute.ts
|-- client
|-- talent
public/client/talentとそれぞれ利用者に向けた単位で切っている
publicの場合は /
clientの場合は /client/xx
talentの場合は /talent/xx というパス設計になっている
/health_checkの場合ファイル名は
healthCheckPublicRoute.tsとなる
controller
-- controller
|-- public
|-- healthCheckPublicController.ts
|-- client
|-- talent
routeディレクトリと完全に一対一となっている
classとして定義し、staticメソッドのみでインスタンス化はしない
controllerのメソッド名は
GET /health_checkであれば、get
POST /health_checkであれば、postとCRUDに名前を合わせる
/health_check/searchという名前であれば、searchというメソッドを生やす
命名規則は
healthCheckPublicControllerとする
response
レスポンスの型を定義している
-- response
|-- models
|-- projectGenreApiResponseModel.ts
|-- public
|-- projectGenrePublicApiResponse.ts
|-- BaseResponse.ts
modelsはレスポンスの共通の型を置く
publicフォルダはそのAPIごとのレスポンス型を定義するエンドポイントにつき一つ作る
application
Applicationレイヤーに該当
Domainを使って、振る舞いを達成するようなコードを書く
Domainに依存することはできるが、Othersレイヤーに依存することはできないので、infra層にアクセスするときは、repositoryをコンストラクター経由で受け取って利用する
-- application
|-- usecase
|-- job
|-- jobFetchAllUseCase.ts
ユースケースごとに実装するので、一つのクラスにつき、publicのメソッドは一つしかはやさない
cli
cliで実行される処理
cronなどの処理が書かれている
ファイルの命名規則は、xxCli.ts
consts
定数ファイル
グローバルな型もこの中に描かれる
ファイル命名規則はxxConsts.ts
namespaceで必ず囲う
export namespace ClientConsts {
export const clientStatusValue = {
init: 0,
active: 1,
} as const;
export type ClientStatus = typeof clientStatusValue[keyof typeof clientStatusValue];
}
converter
変換器
特定の型から特定の型に変換する処理を行う
変換したい型をファイル名、クラス名とする
domain
-- domain
|-- repository
|-- model
|-- type
modelはドメインをクラスとして表す
repositoryはドメインの振る舞いをinterfaceで定義する
exception
Exceptionを表すクラス
グローバルに使うことができる
infra
infrastructure層
-- infra
|-- db
|-- datasource
|-- entities
|-- elasticSearch
|-- datasource
|-- entities
|-- nicoAi
命名規則
dbのentitiesはXXDbEntity.ts, datasourceはXXDbDatasourceとする
lib
外部ライブラリをラップしたり、汎用性の高い処理を書くクラス
この部分の基準は、他のプロダクトにうつしても利用できるかどうかという基準で考える
repository
domain/repositoryの処理を具体的にした部分
最後に
この設計で実際にプロダクトを作っていく中でいくつかの悩みポイントが出てきた
- DBのテーブルすべてをかならずしもDomainにする必要はないのではないか
- その場合はInfra層をcliやapi側で直接呼び出すことになるが、どっちで呼び出すか迷う
- cronで、DBのデータをcsvに変換してアップロードするみたいな処理をusecaseで定義するか微妙
などなど悩ましい部分はありますが、今後作っていくなかで徐々にアップデートしていきたいと思います。
Another worksでは一緒に働ける仲間を探しています