ããã«ã¡ã¯ïŒBC ããŒã ã§ãšã³ãžãã¢ãããŠãã id:d-kimuson ã§ãã æè¿ãåŒããŒã ã§æ§ç¯ãã瀟å
åã Web API ã®ããã¯ãšã³ãèšèšãããã®ã§äºäŸãšããŠç޹ä»ããããšæããŸãã ãã¬ãŒã ã¯ãŒã¯ãšã㊠NestJS ãæ¡çšããŠããŸãããNestJS Way ããã TS Way ãæèããèšèšãããŠããããã®ãšã³ããªã®äž»é¡ã§ããããããTS Backend ã®èšèšäºäŸãšããŠèªãã§ããã ããã°ãšæããŸãã 察象ã·ã¹ãã ã®æŠèŠ ç€Ÿå
ã®ä»ãµãŒãã¹åãã® Web API ã§ãä»ããŒã ã®ãµãŒãã¹ãçµç±ããŠãšã³ããŠãŒã¶ãŒã«å±ãäžéã·ã¹ãã ããŒã å
ã®ãµãŒãã¹ãããããŒã å€ã®ãµãŒãã¹ãããå©ãããæ³å® ããŒã å€ãããå©ãããããããªãããã®ã¹ããŒããå
±æããããšããã¢ãããŒã·ã§ã³ããã â 2023 幎çŸåšã§æšæºç㪠OpenAPI Specification (ä»¥åŸ OAS ãšåŒã³ãŸã) ãå
±æããã ãã®ä»ã®æ¡çšããŠããæè¡ãç°å¢ Node.js v18 ORM ãšã㊠Prisma ãªã NestJS ã? ã·ã¹ãã ã®æŠèŠã§è§Šããããã«ãæ¬ã·ã¹ãã ã§ã¯ OpenAPI ã®ã¹ããŒããåãåºããããšããã¢ãããŒã·ã§ã³ããããŸããã TypeScript ã«ããã Server/Client éã§ã®ã¹ããŒãã®å
±éåãšããŠã¯ tRPC ã frourio çãéžæè¢ãšããŠãããŸãã ããããä»åã®ã·ã¹ãã ã§ã¯ãã¯ã©ã€ã¢ã³ãåŽã¯ TypeScript ãšã¯éããªããããèšèªã«äŸåããªãæšæºçãªã¹ããŒããšããŠæžãåºãå¿
èŠããããOAS ãæ¡çšããŸããã OAS ã¹ããŒããçšæãããšãã¯ãå®è£
ãšã¹ããŒããäžèŽããããšãä¿èšŒãããã ã³ãŒããã¡ãŒã¹ã: å®è£
ãæžããšå®è£
ã«æ²¿ã£ãã¹ããŒããçæããã ã¹ããŒããã¡ãŒã¹ã: (OpenAPI ã®)ã¹ããŒããæžããšå®è£
ã®ãã€ã©ãŒãã¬ãŒã(ãããã¯ãã®ãŸãŸäœ¿ããå®è£
)ãçæã§ããããŸãå®è£
ã«å¯Ÿããå¶çŽã®åå®çŸ©çãçæãããã¹ããŒããæºããå®è£
ãããããªãç¶æ
ã«ãªã ã® 2 ã€ã®ããããã®éžæè¢ãåãã®ãæãŸãããšèããŠããŸãã ä»åã¯å
ã«ã¹ããŒããçšæãããã£ãã®ã§ã¹ããŒããã¡ãŒã¹ãã®ã¢ãããŒãã®æ¹ãæãŸããã£ãã®ã§ãããTS Backend ã«ãããŠã¹ããŒããã¡ãŒã¹ããé©åã«è¡ãã¹ã¿ã³ããŒããªæ¹æ³ãããŸããªããã³ãŒããã¡ãŒã¹ããªã¢ãããŒããæ¡çšãã€ã€ãã€ã³ã¿ãã§ãŒã¹ã ãå
ã«ã³ãŒãã£ã³ã°ãã圢ã§ãå
ã«ã¹ããŒããçšæãããããšããèŠä»¶ãæºããããšã«ããŸããã ã³ãŒããã¡ãŒã¹ãã®ããããããšããŠã NestJS ã¯ã³ã³ãããŒã©ãŒã®åŒæ°ã»æ»ãå€ã®åããã®ãŸãŸ OAS ãæžãåºãããäœéšããšãŠãè¯ãããšã«å ã㊠ããŒã ã§ã®æ¡çšäºäŸããã£ãããš (Express çã®èããã¬ãŒã ã¯ãŒã¯ãšæ¯ã¹) NestJS ã§ã¯å
æ¬çã« Web éçºã«ãããæ©èœããã¬ãŒã ã¯ãŒã¯ãšããŠæäŸããŠããããããWeb éçºå
šè¬ã§å¿
èŠãªæšæºçãªæ©èœãåå®è£
ããããšãªããã¡ã€ã³ã® API éçºã«éäžã§ããããš çã®å©ç¹ãããããšããããã¬ãŒã ã¯ãŒã¯ãšã㊠NestJS ãæ¡çšããŸããã åºæ¬çãªæ¹é å
¬åŒããã¥ã¡ã³ãã»ãšã³ã·ã¹ãã ã®åãã§ãã¯ãªãã·ã§ã³ãšã¯è·é¢ã眮ã NestJS å
¬åŒã®ãžã§ãã¬ãŒã¿ã§ãã€ã©ãŒãã¬ãŒããäœæãããšãTypeScript ã®ããã©ã«ãã§ã¯ strict ãªãã·ã§ã³ã«ãã£ãŠ off ã«ãããŠãã any ã蚱容ãããªãã·ã§ã³ null å®å
šæ§ããªããããªãã·ã§ã³ ãæå¹ã«ãããç¶æ
ã® tsconfig.json ãäœæãããŸãã å
¬åŒããã¥ã¡ã³ãã§ã export class CreateCatDto { name: string age: number breed: string } ã®ãããªã³ã³ã¹ãã©ã¯ã¿ã§ã®åæåããããŠããªã DTO(Data Transfer Object) ãæšæºçã«ç޹ä»ããŠãããstrictNullChecks ã off ã§ããããšãåæãšãªã£ãŠããããšãããããŸãã å¯èœãªéããã¬ãŒã ã¯ãŒã¯ã® Way ãå
¬åŒããã¥ã¡ã³ãã®å
å®¹ã«æ²¿ã£ãŠããããšã¯ãšãŠãéèŠã§ãããå°ãªããšã NestJS åšèŸºã®åãã§ãã¯åšãã®è¡åã¯ããã»ã©è¯ããªãã®ã§ãé©åãªè·é¢æã§ä»ãåã£ãŠããããšã倧äºã ãšæããŸãã 颿°åããã°ã©ãã³ã°ã軞ãšãã NestJS ã¯ãªããžã§ã¯ãå¿åãããŒã¹ãšããä»èšèªã§ãé©çšã§ãããã¬ãŒã ã¯ãŒã¯ã»èšèšã TS ã®äžçã«æã£ãŠãããããªãã¬ãŒã ã¯ãŒã¯ã§ãã *1 æšæºã§ã³ã³ãããŒã©ãŒå±€ããµãŒãã¹å±€ãçµã¿èŸŒã¿ã® DI 解決㫠class ã䜿ã£ããµã³ãã«ãæç€ºããŠããããå®éã« DI æ©èœãæäŸããŠããç¹ããçŽ çŽã«å®è£
ããããšãªããžã§ã¯ãå¿åçãªèšèšã»å®è£
ã«ãªãåŒåãåããã¡ã ãšæã£ãŠããŸãã ããããå人çã«æè»ãªããŒã¿æ§é ã®åãåããããããæ§é çéšååã®åã·ã¹ãã ãæã€ TypeScript ã«ãããŠã¯ãããŒã¿æ§é ãšãµããŸã(ã¡ãœãã)ãã»ããã§å®çŸ©ãããªããžã§ã¯ãå¿åçãªããæ¹ããããåé§åã§ãã¡ã€ã³ã®ããŒã¿æ§é ã宣èšãããµããŸãã颿°ãšããŠåé¢ãã颿°åçãªã¢ãããŒãã®ã»ããçžæ§ãè¯ããšèããŠããŸãã ãããã£ãŠãä»åã® Web API éçºã«ãããŠã¯é¢æ°åããã°ã©ãã³ã°ã®ãšãã»ã³ã¹ã軞ã«èšèšãè¡ããŸããã 颿°åããã°ã©ãã³ã°ãšã¯èšã£ãŠãã倧äºã«ããŠããã®ã¯ ãããããEntityãã«å®çŸ©ãããã¡ãœãããšããŒã¿æ§é ã¯ãåãšé¢æ°ãšããŠãã£ããåé¢ããã *2 å¯äœçšãåé¢ããã 颿°ã¯ãŠããããã¹ããæžããããå°ããåäœã§äœã£ãŠãããããããçµã¿åãããããšã§ããžãã¹ããžãã¯ãæ§æããã ãšããåŽé¢ã«éãã眮ããŠããŸãã äžæ¹ãTypeScript ã«ããã颿°åããã°ã©ãã³ã°ãšããŠã¯ã fp-ts çã®ã©ã€ãã©ãªãæäŸãã 颿°ã®ã«ãªãŒå ( (arg1, arg2) => ret ã (arg1) => (arg2) => ret ã«ãã) 颿°åæã»ãã€ã ( f(g(arg)) ã§ã¯ãªã pipe(arg, g, f) çãªæžãæ¹) çã®æ©èœã䜿ãããšãã§ããŸããããããã£ãæžãæ¹ã»ã©ã€ãã©ãªãšã¯è·é¢ã眮ããŠããŸãã ããã¯ æšæºã® TypeScript ã®æžãå¿å°ãšã¯ããªããºã¬ãŠããŸãã㚠远å ã®åŠç¿ã³ã¹ããçºçããããš æšæºãšã®ãºã¬ã«ããããŒã ã§ã®ã¡ã³ããã³ã¹æ§ã®äœäž ãšãããã¡ãªãããããããã§ãã 颿°åã軞ãšããããã¯ãšã³ãèšèšãšããŠãæžç± Domain Modeling Made Functional ãåèã«ããŠããŸããç¥èŠãå°ãªã颿°åããŒã¹ã®èšèšææ³ãèªãããŠããŠãšãŠãåèã«ãªããŸããã åºæ¬çãªã³ã³ã»ããã¯ä»¥äžã® 2 ç¹ã§ãã ããããã¯ããå
·äœçãªã¢ãŒããã¯ãã£ãæ¹éã«ã€ããŠç޹ä»ããŠãããŸãã åãã§ãã¯ã峿 Œã«ãã ãå
¬åŒããã¥ã¡ã³ãã»ãšã³ã·ã¹ãã ã®åãã§ãã¯ãªãã·ã§ã³ãšã¯è·é¢ã眮ããã§ã玹ä»ããããã«ãå
¬åŒãããã©ã«ãã§æäŸããåãã§ãã¯ã¯ããªãç·©ãç¶æ
ã«ãªã£ãŠããŸãã ä»åã¯ã tsconfig/bases ã® strictest ãªãã·ã§ã³ãããŒã¹ã«ãã€ã€ãNestJS ã§å©çšã§ããããã«ãªãã·ã§ã³ãäžéšäžæžãæå®ããŠããŸãã 以äžã¯å®éã«å©çšããŠãã TS èšå®ã®äžéšã§ãã { " extends ": [ " @tsconfig/strictest ", " @tsconfig/node18 " ] , " compilerOptions ": { " module ": " commonjs ", " declaration ": true , " removeComments ": true , " emitDecoratorMetadata ": true , " experimentalDecorators ": true , " allowSyntheticDefaultImports ": true , " sourceMap ": true , " outDir ": " ./dist ", " incremental ": true , " skipLibCheck ": true , " importsNotUsedAsValues ": " remove " // decorator ã§äœ¿ãã»eslint ã§ããã®ã§ } } ç¹ã«åŸããç·©ãåãã§ãã¯ã硬ãåãã§ãã¯ãžã®ç§»è¡ããŠããã®ã¯å€§å€ã«ãªããŸãã®ã§ã硬ããããããã®ãã§ãã¯ãé©çšããŠéçšã«äœµããŠç·©ãããŠãããããã®ã¹ã¿ã³ã¹ããªã¹ã¹ã¡ããŸãã ãŸãããã®èšäºã§ã¯ç¹ã«æãããªãéã以äžã®åãã§ãã¯ãªãã·ã§ã³ãé©çšããã TS 5.0 ãå©çšããŠããããšãåæãšããŸãã strictNullChecks ãš DTO ãš constructor NestJS ã§ã¯ãªã¯ãšã¹ããã©ã¡ã¿/ããã£ãã¬ã¹ãã³ã¹ããã£ã®åå®çŸ©ã«ã¯ DTO ã䜿ããŸãã DTO ãšã¯ãã¶ã€ã³ãã¿ãŒã³ã®äžçš®ã§ãäžè¬çã«ããžãã¹ããžãã¯(ã¡ãœãã)ãå«ãŸããªãããŒã¿ããããç®±ã®ããšãæããŸããå³å¯ãªå®çŸ©ã¯çœ®ããŠãããŠãNestJS ã®æèã«ãããŠã¯ ã³ã³ãããŒã©ãŒã®åŒæ°(ãªã¯ãšã¹ããã©ã¡ã¿ã»ããã£)ãæ»ãå€(ã¬ã¹ãã³ã¹ããã£)ã®ã€ã³ã¿ãã§ãŒã¹ã宣èšãã class ã§ãã åŒæ°ã«é¢ã㊠class-validator ã®ãã³ã¬ãŒã¿ãæžããšåæã«ããªããŒã·ã§ã³ãããŠãããŠãã class ã§ãã *3 @nestjs/swagger ã«ãã£ãŠãåŒæ°ã»æ»ãå€ãããããã£ã®åå®çŸ©ãã OAS ã«æžãåºããŠããã class ãšãã£ãæå³åããæã£ãŠããŠãã€ã³ã¿ãã§ãŒã¹å®£èšã«å ããŠããªããŒã·ã§ã³ãOASæžãåºãã®è²¬åãæã€ç¹æ®ãª class ã ãšæã£ãŠããããã°è¯ãã§ãã strictNullChecks ãªãã·ã§ã³ãæå¹ãªç¶æ
ã§ã¯å
¬åŒããã¥ã¡ã³ãã«æç€ºãããŠãã DTO ã®ãµã³ãã« export class CreateCatDto { name: string age: number breed: string } ã䜿ããšãconstructor ã§ããããã£ãåæåããŠããªãããåãšã©ãŒãçºçããŸãã constructor ãã¡ãããšæžããŠãããã®ãçæ³çã§ããã class-validator ã®ãã³ã¬ãŒã¿ãš Parameter Properties ã®äœµçšãã§ããªãã®ã§ export class CreateCatDto { @IsString () name: string @IsNumber () age: number @IsString () breed: string public constructor( name: string , age: number , breed: string ) { this .name = name this .age = age this .breed = breed } } ã®ãããªéåžžã«åé·ãªèšè¿°ã«ãªã£ãŠããŸããŸãã åŒããŒã ã§ã¯ Non-Null Assertion Operator ã䜿ã£ãŠ class CreateCatDto { name ! : string age ! : number breed ! : string } ã®ããã«ããŠåãšã©ãŒãåé¿ããããšã«ããŠããŸãã ãŸããããŒã ã§ã¯ Dto ã®äœ¿ãæ¹ã«é¢ããŠã2 ã€ã®èŠçŽãæ·ããŠããŸãã â Dto 㯠abstract class ã§å®£èšããããš â¡ Dto ã controller, dto 以å€ã®ãã¡ã€ã«ããåç
§ããªãããš â ãæ·ããŠããã®ã¯ ã³ã³ãããŒã©ãŒã®æ»ãå€ã¯ Dto ã®ã¯ã©ã¹ã€ã³ã¹ã¿ã³ã¹ã§ã¯ãªããã¬ãŒã³ãªããžã§ã¯ããè¿ãããã«çµ±äžãã ããšãç®çã§ãã TypeScript ã§ã¯ãDto åã®ã¯ã©ã¹ã®æ»ãå€ãæå®ãããŠãããšãã«ãå®éã«ã¯ã¯ã©ã¹ã€ã³ã¹ã¿ã³ã¹ã§ã¯ãªããã¬ãŒã³ãªããžã§ã¯ããè¿ããŠãåãšã©ãŒã«ãªããªããšããæåã«ãªããŸãã import { plainToClass } from "class-transformer" class SomeController { // class ã private ãªãã£ãŒã«ããæããªããšãã«ç¶æ¿é¢ä¿ã§ã¯ãªãããããã£ã®æ§é ã§åãã§ãã¯ããã仿§ãå©çšããŠãã¬ãŒã³ãªããžã§ã¯ããè¿ããã¿ãŒã³ createCatV1 () : CreateCatDto { return { name: "nyash" , age: 3 , breed: "A" , } } // CreateCatDto ã®ã€ã³ã¹ã¿ã³ã¹ãäœæããŠã€ã³ã¹ã¿ã³ã¹ãè¿ãããããããã£ã¯ Object.assign ã§å²ãåœãŠããã¿ãŒã³ createCatV2 () { return Object .assign (new CreateCatDto (), { name: "nyash" , age: 3 , breed: "A" , } ) } // CreateCatDto ã®ã€ã³ã¹ã¿ã³ã¹ãäœæããŠã€ã³ã¹ã¿ã³ã¹ãè¿ãããããããã£ã¯ plainToClass ã§å²ãåœãŠããã¿ãŒã³ createCatV3 () { return plainToClass (new CreateCatDto (), { name: "nyash" , age: 3 , breed: "A" , } ) } } äžèšã® createCatV1 ã§ã¯ãã¬ãŒã³ãªããžã§ã¯ããè¿ãã createCatV2 , createCatV3 ã§ã¯ã¯ã©ã¹ã€ã³ã¹ã¿ã³ã¹ãè¿ããŠããŸããã©ã¡ããåãã§ãã¯ãéãæ£ããã³ãŒãã§ãã ãããããããã蚱容ãããæ··åšããç¶æ
ã§ã¯ ãã¹ãã§ toBeInstanceOf ã§å€å®ã§ããªãã£ãã ã³ãŒãã£ã³ã°ããäžã§ã€ã³ã¹ã¿ã³ã¹ãªã®ãããã¬ãŒã³ãªããžã§ã¯ããªã®ããæèããå¿
èŠããããèªç¥è² è·ãå¢ãã ãšãã£ãèŸããæ³å®ãããã®ã§ãããã¬ãŒã³ãªããžã§ã¯ããè¿ãããšãã«äžè²«æ§ãããããããã« â Dto 㯠abstract class ã§å®£èšããããš ã®èŠçŽãæ·ããŠããŸãã â¡ Dto ã controller, dto 以å€ã®ãã¡ã€ã«ããåç
§ããªãããš ã«é¢ããŠã¯ãDto ã¯ãããŸã§ãOAS ãæžãåºããã class-validator ã䜿ãããããããã«ãã€ã³ã¿ãã§ãŒã¹ã欲ããã ãã ãã©ãããªã class ã䜿ãå¿
èŠãããããšããã ããªã®ã§ãå¿
ç¶æ§ã®ããã³ã³ãããŒã©ãŒå±€ä»¥å€ããã¯äœ¿ãã®ã¯ãããŸããããããšãããã®ã§ãã DTO ã® class ãšã®åãåãæ¹ã¯ä»åã®æ¹é以å€ã«ãããã€ãèãããã *4 ãšæããŸããããããã«ããåãã§ãã¯äžã®æã穎ã«ãªããããã®ã§ã³ãŒãã£ã³ã°èŠçŽãã¬ã€ãã©ã€ã³çã§ã¹ã¿ã³ã¹ãæç¢ºã«ããŠããããšãæãŸãããšæããŸãã ãã¡ã€ã³ã¢ãã«ã¯ class ã§ã¯ãªãåäžãã¡ã€ã«å
ã«å®£èšãã type ãšé¢æ°ã«ãã£ãŠå®£èšããã ãã class ã§è¡šçŸããã Entity ã¯ãäžè¬çã«ããŒã¿ãšé¢é£ããã¡ãœãã矀ãæã¡ãŸãã class UserEntity { public constructor( public id: number , public firstName: string , public lastName: string , public birthDate: Date , public isAuthenticated: boolean , public createdAt: Date ) {} public get fullName () : string { return this .firstName + " " + this .lastName } } 以äžã®ãã㪠Entity ã¯åãšé¢æ°ã«åé¢ããŠä»¥äžã®ããã«å®£èšããŸãã /* Orm ãããããã³ã°ãããå */ type User = { id: number firstName: string lastName: string birthDate: Date isAuthenticated: boolean createdAt: Date } /** * ãã¹ãŠã§ã¯ãããŸããããDB ã®ã¬ã³ãŒããšãã¡ã€ã³ã¢ãã«ã察å¿é¢ä¿ã«ãªãããšãå€ããšæããŸã * ããããã±ãŒã¹ã§ã¯ä»¥äžã®ãããªåœ¢ãåã£ãŠãããšäŸ¿å©ã§ã * * @example UserEntity<{ posts: Post[] }> -- ãªã¬ãŒã·ã§ã³ãæã€ããŒã¿æ§é ã®ãã¿ãŒã³ã ã£ãã * @example UserEntity<{ tag: 'Validated' }> -- åãããŒã¿æ§é ã§ããªããŒã·ã§ã³æžã¿çã®ã©ã€ããµã€ã¯ã«ã衚ãã¿ã°ãä»å ããã */ export type UserEntity < AdditionalFields extends Record < string , unknown > = {} > = User & { age: number } & Omit < AdditionalFields , keyof User >; export const buildUserEntity = < T extends User >( data: T ) : UserEntity < T > => { return { ...data , age: age ( data.birthDate ), } satisfies UserEntity as UserEntity < T > } ; /** * @desc * - ã¡ãœããã«è©²åœããåŠç㯠`Pick<EntityType, äŸåããããããã£ã®äžèЧ>` ã第äžåŒæ°ã«åã颿° * - ããããããšã§å¿
èŠãªããããã£ãæç€ºãããã®ãšãéšåå(äŸ: `Omit<UserEntity, 'age'>`)ã«å¯ŸããŠãæäœéã®ããããã£ãæã£ãŠããã°åŒã¹ãããã«ãªã */ const fullName = ( user: Pick < UserEntity , 'firstName' | 'lastName' >) : string => user.firstName + ' ' + user.lastName åãšé¢æ°ãåé¢ãããŠããã®ã§ãããžãã¯ãå¥ãã¡ã€ã«ã«æã£ãŠããããšãã§ããŸããããã¡ã€ã³ã¢ãã«ãšåããã¡ã€ã«å
ã«ããããŠããã»ããåéã§ãèªã¿ãããã®ã§åãã¡ã€ã«å
ã«çœ®ããŠããŸãã ããŒã¿åãšé¢æ°ãåé¢ãããŠããããšã§ãclass ã§ã¯åé·ãªèšè¿°ãå¿
èŠã ã£ãåã䜿ã£ãå¶çŽã®è¡šçŸããããããªããŸãã äŸãã°ãããªããŒã·ã§ã³ééæžã¿ã®ããŒã¿ã§ã®ã¿ãŠãŒã¶ãŒäœæã®ããžãã¯ãå®è¡ã§ãããã以äžã®ããã«åã§è¡šçŸã»å¶çŽãã€ããããšãã§ããŸãã type ValidatedUserEntity = UserEntity < { tag: "VALIDATED" } > // service const validateUser = ( user: UserEntity ) : ValidatedUserEntity => { // ããªããŒã·ã§ã³ãééããããŒã¿ã«ã®ã¿ã¿ã° 'VALIDATED' ãä»å ãã return { tag: "VALIDATED" , ...user , } } const createUser = async ( validatedUser: ValidatedUserEntity ) => { // ... } declare const user: User const userEntity = buildUserEntity ( user ) // validateUser ãééããŠããªãããŒã¿ã§ã¯åŒã³åºããªã createUser ( userEntity ) // Argument of type 'UserEntity<User>' is not assignable to parameter of type 'ValidatedUserEntity'. const validatedUser = validateUser ( userEntity ) createUser ( validatedUser ) // åãšã©ãŒãªãã§åŒã³åºãã ä»ã«ãäŸãã°ããªããŒã·ã§ã³ãééããŠèªèšŒæžã¿ã«çµãããŠããããŒã¿ã§ããã° type AuthenticatedUser = UserEntity & { isAuthenticated: true } ã®ããã«åãäœã£ãŠå¶çŽãšããŠå©çšããããšãã§ããŸãã ãã®ããã«åã User ãã¡ã€ã³ã¢ãã«ã§ãäžé£ã®æç¶ãã®ã¿ã€ãã³ã°æ¯ã§åãããããŒã¿æ§é ã¯ä»£ãããŸãã ããããã®ã¹ããããåºå¥ããã»å³æ Œãªåã§è¡šçŸã§ãããšãæé»çã«ã§ã¯ãªãåãã§ãã¯ã§åŒã³åºãã®å¶çŽçãå¶åŸ¡ããããšãã§ããŸãã class ã§ã¯ãã®èŸºã®åãåãããèŸãã§ãããåãšé¢æ°ã«åé¢ããŠãããããšã§æè»ã«ã»åé§åã§ããžãã¹ããžãã¯ãèšè¿°ããŠããããããªããŸãã ã¡ãœããã«å¯Ÿå¿ãã颿°ã Pick<UserEntity, 'firstName' | 'lastName'> ã®ããã«å¿
èŠãªããããã£ã ãåæããããšã§ãããå¶çŽã®åŒ·ãéšåå(äŸãã° AuthenticatedUser )ã§ãæäœéã®ããããã£ã ãæã£ãŠããã°åŒã³åºãããšãã§ããŸãã å¯äœçšãåé¢ãã 颿°åããã°ã©ãã³ã°ã§å€§äºã«ããããšãã»ã³ã¹ã® 1 ã€ãšããŠå¯äœçšã®åé¢ããããŸãã å¯äœçšãšã¯ããªãã¡ã颿°ã®æ»ãå€ãè¿ã以å€ã®å¹æãã®ããšã§ããããããããšããã§ HTTP éä¿¡ã»DB ã® Read/Writeã»ãã®ã³ã°çããããŸãã å¯äœçšã®ç¹åŸŽãšã㊠å€éšç¶æ
ã«äŸåãããããåŠçãå®å®ãã¥ãã äŸãã°æ£ããå®è£
ããã颿°ãäŸåãã DB ã HTTP éä¿¡å
ã®ç¶æ
ã«åœ±é¿ãããŠå£ãããããå¯èœæ§ããã å€éšã®ç¶æ
ã«äŸåãããããã¹ã¿ããªãã£ãäœã äºåã«äŸåç¶æ
ãçšæããå¿
èŠãããããã¹ãã±ãŒã¹ã颿°ã®å
¥å â åºåã§ã¯ãªããäºåã® DB ç¶æ
â 颿°ã®åºå(ãããã¯äºåŸã® DB ç¶æ
)ã«ãªãããã¹ãã±ãŒã¹ãèªã¿ã¥ããåŸåã«ãã äžæã DB ãã»ããã¢ãããããªãã»ãã¹ãã±ãŒã¹åäœã§ã® DB åé¢ãå£ããçã察象ã®ãã¹ãã±ãŒã¹ãšã¯çŽæ¥é¢ä¿ãªãã¬ã€ã€ãŒã®åé¡ã§ãã¹ãããã¬ã€ããŒã«ãªãããã (DB ã®å¯äœçšã«ã€ããŠ) å¯äœçšãããã«ããã¯ã«ãªãããã¹ãã®å®è¡æéãé·ããªã DB æ¥ç¶ãããã«ããã¯ã«ãªã èåçãšããŠã€ã³ã¡ã¢ãª DB ãžã®å·®ãæ¿ãçã®ã¢ãããŒããããããæ ¹æ¬çãªè§£æ±ºã«ã¯ãªããããŸãæ¬çªãšç°ãªãç°å¢ã§ãã¹ããåãããšã«ãªã£ãŠããŸã äŸåãã DB ã®ç¶æ
ããã¹ãã±ãŒã¹ããšã«åé¢ãããŠããå¿
èŠããããã䞊åã§ã®ãã¹ãå®è¡ããã¥ãããªã ãšããã€ããåŽé¢ããããŸãã ä»åã®ã¢ããªã±ãŒã·ã§ã³ã§ã¯å
šäœã®ã¢ãŒããã¯ãã£ã¯ã¬ã€ã€ãŒæ§é ãåã£ãŠããŠãã¬ã€ã€ãŒã¬ãã«ã§å¯äœçšã蚱容ããå±€ãšããªãå±€ãåé¢ããããšã§ãã€ããç¯å²ãçããŠããŸãã ãã ããå¯äœçšã®åé¢ãšèšã£ãŠãŸããçŽç²é¢æ°åã®èšèªã§ãªã TypeScript ã§ã¯å³å¯ã«ãã¹ãŠã®å¯äœçšãåé¢ããããšã¯é£ããã³ã¹ããèŠåããªãã®ã§èããŠãããã ç¹ã«åœ±é¿ã®å€§ãããå€éšãšã® HTTP éä¿¡ãã ãDB æ¥ç¶ããå¯äœçšãšèã㊠åé¢ããŠããŸãã ç¹ã«è£è¶³ããªãéãããã®èšäºå
ã§å¯äœçšãšèªãã ãšãã¯ããããæããã®ãšããŸãã *5 ã¬ã€ã€ãŒæ§é ãšå¯äœçš ã¢ããªã±ãŒã·ã§ã³ã§æ¡çšããŠããã¬ã€ã€ãŒæ§é ã¯ä»¥äžã®éãã§ã å±€ 説æ å¯äœçš domain-object äžã®ã»ã¯ã·ã§ã³ã§èª¬æãããã¡ã€ã³ã¢ãã«(åãšã¢ãã«ã«éãã颿°)ãå
¥ãå±€(é¢å¿ããã¡ã€ã³ã¢ãã«) ãªã ãµãŒãã¹å±€ è€æ°ã®ãã¡ã€ã³ã¢ãã«ã«è·šãããžãã¹ããžãã¯ãå®è£
ããå±€(é¢å¿ãã·ã¹ãã ) ãªã ãªããžããªå±€ HTTP éä¿¡ã DB äŸåããããšãã«ããŒã¿ãã§ãããè¡ãå±€ ãã ãŠãŒã¹ã±ãŒã¹å±€ ãµãŒãã¹å±€ã»ãªããžããªå±€ã»domain-object ã®ããžãã¯ãçµã¿åãããŠæå³ã®ãããŠãŒã¹ã±ãŒã¹ãå®è£
ããå±€ã§ãå±€ ãªã ã³ã³ãããŒã©ãŒå±€ HTTP ãªã¯ãšã¹ããåããŠã¬ã¹ãã³ã¹ãçµã¿å»ºãŠãå±€ã§ã ãã äŸåé¢ä¿ã¯ä»¥äžã®ããã«ãªããŸãã ãŠãŒã¹ã±ãŒã¹å±€ã¯å³å¯ã«ã¯å¯äœçšãæã€å Žåãå€ãã§ãããåŸè¿°ãã Dependency Injection ã䜿ãããšã§ãèŠããäžå¯äœçšããªãç¶æ
*6 ã«ããŠããŸãã jest.config ã¬ãã«ã§ãã¹ãã 2 çš®é¡ã®ç³»çµ±ã«åé¢ãã å¯äœçšãåé¢ããã 1 ã€ã®å€§ããªçç±ãšããŠããã¹ã¿ããªãã£ãé«ãããããšããç¹ããããŸããã ããã§ãå±€ããšã«å¯äœçšãæ±ãã«ãŒã«ãåé¢ãããŠããããšãã jest.config ã¬ãã«ã§å¯äœçšãæã€å±€ã®ãã¹ããå¯äœçšãæããªãå±€ã®ãã¹ããåé¢ããŠããŸãã 以äžã¯å®éã«äœ¿ã£ãŠãã jest.config ã®æç²ã§ãã // jest.config.pure.ts import type { Config } from "jest" ; import { baseConfig } from "./jest.config.base" ; const config = { ...baseConfig , testMatch: [ "**/usecases/**/*.spec.ts" , "**/services/**/*.spec.ts" , "**/domain-object/**/*.spec.ts" ] } satisfies Config ; export default config ; // jest.config.with-side-effects.ts import type { Config } from "jest" ; import { baseConfig } from "./jest.config.base" ; const config = { ...baseConfig , testMatch: [ "**/*.controller.spec.ts" , "**/repositories/**/*.spec.ts" ] , setupFilesAfterEnv: [ ...baseConfig.setupFilesAfterEnv , // DB äŸååŽã¯ã»ããã¢ããã DBãªã»ããçã® setup ãå¿
èŠã«ãªã "./src/test-utils/setup/separate-db-per-worker.ts" , "./src/test-utils/setup/setup-prisma.ts" , "./src/test-utils/setup/reset-table.ts" , ] } satisfies Config ; export default config ; ããšã¯ { " scripts ": { " test:pure ": " jest --config ./jest.config.pure.ts ", " test:with-side-effects ": " jest --config ./jest.config.with-side-effects.ts " } } ã®ãã㪠npm-scripts ãçšæããŠããããšã§ $ pnpm test:pure # DB éäŸåã®ãã¹ãã®ã¿å®è¡ããã $ pnpm test:with-side-effects # DB äŸåã®ãã¹ãã®ã¿å®è¡ããã ã®ãããªåœ¢ã§åé¢ããŠæå
ã§ãã¹ããå®è¡ã§ããŸãã test:with-side-effects åŽã¯ ãã¡ã€ã«ããšã« jest ã® worker åäœã§äžŠååãããŠããŸããããã¡ã€ã«åäœã§ã¯åå¥ã®ãã¹ãã±ãŒã¹ãçŽåã«åãããš åè: Prisma ã§æ¬ç©ã® DBMS ã䜿ã£ãŠèªåãã¹ããæžã - mizdra's blog ãã¹ãã±ãŒã¹ã®ãã³ã« DB ã®ãªã»ããåŠçãå
¥ãããš ã® 2 ç¹ãçç±ã§ã仮㫠DB ã®å¯äœçšããªãã£ããšããŠããã¹ãã±ãŒã¹ããšã«ãªãŒããŒããããçºçããæ§æã«ãªã£ãŠããŸãã test:pure ã test:with-side-effects ããåé¢ãããŠããããšã§ã test:pure åŽã«äžèŠãªãªãŒããŒããããããããã軜éãªãã¹ããšããŠæå
ã§å®è¡ãããããªããTDD çãªéçºããããããªããšèããŠããŸãã test:pure åŽã§ã¯ã€ã³ãã©ã¹ãã©ã¯ãã£ãžã®é¢å¿ããªãããžãã¹ããžãã¯åŽã«é¢å¿ã眮ãããŠããããããã®ã¬ã€ã€ãŒã軜éã«ã»é«éã«ãã¹ãã§ãããšãšãŠãäœéšãè¯ãã§ãã çŸæç¹ã§ã®èšæž¬å€ãšããŠã¯ãåŸè
㯠90 ãã¹ãã±ãŒã¹ãããã«ãã¹ãã§ 82 ç§çšããããŸãããåè
㯠126 ãã¹ãã±ãŒã¹ã 2.5 ç§çšã§å®è¡ã§ããŸãã å¯äœçšã®ãµã³ãã€ããã«ããåé¢ åºæ¬çã«ãµãŒãã¹å±€ã»domain-object ã¯ããŸãæèãããšãå¯äœçšã®ãªã颿°ã«ãªã£ãŠãããããã§ããããŠãŒã¹ã±ãŒã¹å±€ã¯ DB ã«äŸåããããšãå€ãã®ã§çµæ§å³ããã§ãã å¯äœçšã¯ã§ããã ãåé¢ãããã®ã§ ã®ããã«ãªã¯ãšã¹ãã®ååŸã«å¯äœçšãéçŽã§ãããããªåœ¢åŒãåããã®ã§ããã°ãããçæ³çã§ãã ãã ããè€éãªãŠãŒã¹ã±ãŒã¹ã«ãªã£ãŠãããšãDB ããååŸããå€ãå
ã«ä»ã®å€ã DB ããå€ãååŸããå¿
èŠãããå ŽåããåŠçã®éäžã§å¯äœçšãæãŸãããåŸãªãã±ãŒã¹ããããŸããããããã±ãŒã¹ã§ã¯ãŠãŒã¹ã±ãŒã¹å±€ããå¯äœçšãæé€ããããšã¯é£ãããããDependency Injection ã䜿ã£ãŠãããŸãã 颿°ããŒã¹ã® Dependency Injection Dependency Injection ãšèããšã¯ã©ã¹ããŒã¹ã® DI ãæãæµ®ãã¹ã人ãå€ããšæããŸãããä»åã¯é«é颿°ã䜿ã£ã DI ã䜿ã£ãŠãããŸãã Injectable ãªé¢æ°ã宣èšããããã®ãŠãŒããªãã£é¢æ°ãçšæããŸãã type ArgType < T extends Function > = T extends ( ...args: infer I ) => any ? I : never ; type InjectableFn = ( ...args: any ) => ( ...args: any ) => any ; export type IInjectable < Deps extends Record < string , ( ...args: any ) => any >, Args extends ReadonlyArray < unknown > = [] , Ret = void > = ( deps: Deps ) => ( ...args: Args ) => Ret ; export const defineInjectable = < DepFns extends Record < string , InjectableFn >, Deps extends Record < string , ( ...args: any ) => any > = { [ K in keyof DepFns ] : ReturnType < DepFns [ K ] >; } >( _depFns: DepFns ) => < Fn extends ( deps: Deps ) => ( ...args: any ) => any >( fn: Fn ) : Fn => fn satisfies IInjectable < Deps , ArgType < Fn >, ReturnType < Fn >>; äžèº«ã®èª¬æã¯éèŠã§ã¯ãªãã®ã§çœ®ããŠãããŸãããé«é颿°ã䜿ã£ãŠ (ORM) => (åŒæ°) => Promise<ååŸããå€> ã®åœ¢åŒã§å®è£
ããããªããžããªå±€ã® DI 解決ããããŠãŒãã£ãªãã£ã§ãã 以äžã®ãŠãŒããªãã£ã䜿ã£ãŠããŠãŒã¹ã±ãŒã¹å±€ã¯ä»¥äžã®ããã«æžããŸãã // repository ããããªæãã§å®çŸ©ãããŠããšã㊠const findUserById = ( prisma: PrismaClient ) => async ( id: number ) => prisma.user.findUnique ( { where: { id , } , } ) // ãŠãŒã¹ã±ãŒã¹ã¯ããå®çŸ©ããŠãããŸã const loginUsecase = defineInjectable ( { findUserById /* äŸåãããªããžããªå±€, (prisma) => (arg) => Promise<Data> */ , } )( ( deps /* DI ã解決ããã (arg) => Promise<Data> ãåãåã */ ) => async ( id: number ) => { const user = await deps.findUserById ( id ) // ... } ) ã³ã³ãããŒã©ãŒå±€ããã¯ããªããžããªå±€ã« prismaClient (ORM Client) ã Inject ããŠãããŠãã®ãŸãŸåŒã³åºããŸãã const result1 = loginUsecase ( { findUserById: findUserById ( prismaClient ), } )( 1 ) ãã¹ãããã¯ãªããžããªå±€ãå® DB äŸåã®å
ã®é¢æ°ãããã¹ãçšã®ãã®ã«å·®ãæ¿ããŠãããããšã§å® DB ãžã®äŸåãã¯ããããšãã§ããŸãã // ãã¹ãã§ã®åŒã³åºãæ¹ const result = loginUsecase ( { findUserById: async () => ( { id: 1 , firstName: "yamada" , lastName: "taro" , } ), } )( 1 ) é«é颿°ã§ã® DI ã䜿ãããšã§å¯äœçšããªã(åãé€ããã)ç¶æ
ãäœãããšãã§ããŸããã æç€ºãããµã³ãã«ã³ãŒã㯠ãã¡ã ã§è©Šãããšãã§ããŸãã äŸå€æŠç¥ TypeScript ã«ãããäŸå€ã¯çµã¿èŸŒã¿ã® throw ããããŸãã 颿°ã®ã€ã³ã¿ãã§ãŒã¹ããã©ããªäŸå€ãæãããããããªã ãšã©ãŒãã³ããªã³ã°ãæŒããå¯èœæ§ããã ãšããã€ãããããã代æ¡ãšããŠäž»ã« 2 çš®é¡ã®ä»ã®ã¢ãããŒããæå±ãããŠããŸãã çµã¿èŸŒã¿ã®äŸå€ã throw ã§ã¯ãªã return ããããš ãµãŒãããŒãã£ã©ã€ãã©ãªçã䜿ã£ã Result åã䜿ãããš æ£åžžç³»ã»ç°åžžç³»ãã©ããããæ§é result.isOk() ? result.value : result.err ã®ãããªã€ã³ã¿ãã§ãŒã¹ã§æ£åžžç³»ã«ã¢ã¯ã»ã¹ãããå ãããã®ã¢ãããŒãã§ã¯ããããç°åžžç³»ãæ»ãå€ãšããŠé¢æ°ã®ã€ã³ã¿ãã§ãŒã¹ã«çŸããã®ã§ã颿°ãèŠãã°ç°åžžç³»ããããããšã»ç°åžžç³»ã®ãã³ããªã³ã°ãæŒããªãç¹ã§ throw ã®ã€ãããã€ã³ããè§£æ¶ãããŠããŸãã throw ã¯çŠæ¢ãã¹ããïŒ throw ã¯åå®å
šãªãšã©ãŒãã³ããªã³ã°ææ³ã§ã¯ãªãã®ã§ãç¹ã«ããžãã¹ããžãã¯ã®æ¿ãå±€ã§ã¯ã§ããã ãåå®å
šãªææ³ãåãããã§ãã äžæ¹ãthrow ã«åªããŠããç¹ããªãããšãããšãããªããšã¯ãªã㊠ãã³ãŒããã·ã³ãã«ã«ãªãã ãšãããšãŠã倧ããªã¡ãªããããããŸãã return Error ã Result ã®ã¿(throw çŠæ¢)ã§ã³ãŒããã¡ãã£ãšæžããŠã¿ããšããããã§ãããããã¯ããã§ãšãŠãã€ããã§ããReact ã Vue ã§ props ã®ãã±ããªã¬ãŒã€ãããããã¿ãããªè©±ããããŸããåçš®ã®èŸãããããäŸåãã颿°ãã颿°ãžã²ããã Error (ããã㯠Result ) ããã±ããªã¬ãŒããå¿
èŠãåºãŠããŸãã ãã®ç¹ throw ã ãšäžçªäžã§æããåŸãäžçªäžã®ã³ã³ãããŒã©ãŒå±€ãããã¯ããã«ãŠã§ã¢ãªãã§ catch ããŠç°åžžã¬ã¹ãã³ã¹ãè¿ãã°è¯ãã®ã§ãããžãã¹ããžãã¯ãæžããŠãå±€ã®ã³ãŒãããšãŠããã£ããããŸãã ãã®ã¡ãªããã¯çµæ§å€§ãããšèããŠã㊠åå®å
šãªãšã©ãŒãã³ããªã³ã°ããããã±ãŒã¹(åŒã³åºãå
ã§ãã³ããªã³ã°ãæåŸ
ããã±ãŒã¹) ããã§ãªãã±ãŒã¹ ããã£ããå®çŸ©ããŠäœ¿ãåããŠããããšããã¹ã¿ã³ã¹ãåã£ãŠããŸãã Result åã¯äœ¿ãã return Error ãã Result å㯠return Error ãšåãã ã€ã³ã¿ãã§ãŒã¹ããæ³å®ãããäŸå€ãèªã¿åãã å©çšåŽã«ãã³ããªã³ã°ã匷å¶ããããã ãšããã¡ãªãããæäŸããŸãããäŸå€ã·ã¹ãã ã«æšæºã§ãªãã©ã€ãã©ãªãå©çšããŠåŒ·ãäŸåããããšã«ãªãã®ã§ãæšæºã®æ©èœã ãã§å®çŸã§ãã return Error 圢åŒãæ¡çšããŠããŸãã颿°åã®ãšãã»ã³ã¹ã¯äœ¿ãããfp-ts ã䜿ããªãçç±ãšåæ§ã§ãã return Error ãã¿ãŒã³ã䜿ãå Žåã®æ³šæç¹ãšããŠãprivate ãªããããã£ãæããªã class ã¯ç¶æ¿é¢ä¿ã§ã¯ãªãããŒã¿æ§é ã§åãã§ãã¯ãããŠããŸãããåãã§ãã¯äžã®æã穎ãçºçããŠããŸãåé¡ããããŸã( åè )ã ãããåé¿ããããã« const errorSymbol = Symbol () export abstract class BaseError extends Error { private readonly [ errorSymbol ] : undefined } ã®ãã㪠Base ãšãªããšã©ãŒãçšæããŠããããããç¶æ¿ãã圢ãåãã®ããªã¹ã¹ã¡ã§ãã throw ãš return Error ã®æ£²ã¿åã throw ãš return ããäŸå€ã®äœ¿ãåãã«ã€ããŠã§ããããã®ãããžã§ã¯ãã§ã¯äŸå€ã以äžã® 4 çš®é¡ã«åé¡ããŠããŸãã SubNormalError æºæ£åžžç³»ãªäŸå€ã§ãäŸãã°ããªããžããªå±€ã§ã¬ã³ãŒããªãã£ãããšããUnique å¶çŽã«ã²ã£ããã£ãããšããããªããŒã·ã§ã³ééã§ããªãã£ããçã® ä»æ§ãšããŠæ³å®ãããäŸå€ ãæã AbnormalError ç¡æ¡ä»¶ã« 500 ãè¿ããŠè¯ããã® çºçæ¡ä»¶ãªã©æ³å®ãããŠã¯ããããã¢ããªã±ãŒã·ã§ã³ãšããŠç°åžžãªç¶æ
ã§ DB æ¥ç¶ã確èªã§ããªããäŸåããå€éš API ãåäœããŠããªãçã çºçæ¡ä»¶ã¯æ³å®ãããŠãããã¢ããªã±ãŒã·ã§ã³ãšããŠç°åžžãªäŸå€ ãæã UnExpectedError ãã®åå²ãéãããšèªäœã éçºæç¹ã§æ³å®ãããªãäŸå€ ãæã ç¶²çŸ
ãã§ãã¯ããåã·ã¹ãã äžã¯éãåå²ã ãã©å®éã«ã¯ããããªãå Žæç HttpException æãããšæå®ã®ã¬ã¹ãã³ã¹ãè¿ããäŸå€ ãã㊠AbnormalError ãš UnExpectedError ã¯ãã³ããªã³ã°ã匷èŠãããã¡ãªãããå°ãªããäŸå€ã®ãã±ããªã¬ãŒã®ãã¡ãªããã ãåãã圢ã«ãªã£ãŠããŸãã®ã§ throw ãã SubNormalError ã«ã€ããŠã¯åŒã³åºãå
ã§ãã³ããªã³ã°ã匷å¶ããããã®ã§ return ãã HttpException 㯠Usecase/Controller ããã®ã¿ã€ã³ã¹ã¿ã³ã¹ååã³ throw ã§ãã ãšããæ¹éãåã£ãŠããŸãã ããã§ãåå®å
šã«ãã³ããªã³ã°ããã¡ãªãããèãç®æã®ã¿ throw ã§ã³ãŒãããŒã¹ãã·ã³ãã«ã«ä¿ã¡ã€ã€ããã以å€ã®å Žæã§åå®å
šã«(ãã³ããªã³ã°ã匷èŠãããç¶æ
ã§)äŸå€ãæ±ãããšãã§ããããã«ãªããŸããã ãã®ä» Tips åºæ¬çãªã¢ãŒããã¯ãã£èšèšã®ã³ã³ã»ããã¯ä»¥äžã«ãªããŸããããã®ä»äŸ¿å©ãª Tips ãããã€ã玹ä»ããŸãã eslint ã§åç
§ã«ãŒã«ãèšå®ãã ã³ãŒãã£ã³ã°ã«ãŒã«ãå®ããããªãã¹ã eslint ã§åŒŸããããã«ã«ãŒã«ãèšå®ããŠããŸããã³ãŒãã¬ãã¥ãŒã§ææãå
¥ãããå¹çãè¯ãããšãšã匷å¶åããã匷ãããã§ãã eslint-import-plugin ã® no-restricted-paths ã䜿ããšã¬ã€ã€ãŒæ§é ã®åç
§ã«ãŒã«ã宣èšã§ããŸãã åŒç€Ÿã§ã¯ DTO ã Controller/Dto ãã¡ã€ã«ä»¥å€ã§äœ¿ããªã Usecase ããå¥ã® Usecase ãåŒã°ãªã Service ãã Repository ãåŒã°ãªã ã®ã«ãŒã«ã eslint ã§è¡šçŸããŠèšå®ããŠããŸãã .eslintrc.cjs { rules: { "import/no-restricted-paths" : [ "error" , { zones: [ { from: "./src/**/*.!(controller|dto).ts" , target: "**/*.dto.ts" , message: "Dto 㯠Controller ã®åŒæ°ã»æ»ãå€ã§ã®ã¿äœ¿çšãèš±å¯ãããŠããŸã" , } , { from: "./src/**/usecases/*.!(spec).ts" , target: "./src/modules/**/usecases/*.ts" , message: "Usecase å±€ããå¥ã® Usecase å±€ã®åç
§ã¯èš±å¯ãããŠããŸãã" , } , { from: "./src/**/repositories/*.ts" , target: "**/features/**/services/**/*" , message: "Service å±€ãã Repository å±€ãžã®åç
§ã¯èš±å¯ãããŠããŸãã" , } , ] , } , ] , } } ãªãã©ã«åãšãŠããªã³åã¯ã«ã¹ã¿ã ApiDecorator ã䜿ã NestJS ã® OAS æžãåºãã®æ©èœã¯äŸ¿å©ã§ãããå¶çŽãããã€ããããŸãã â ããããã£ã®åã type ã interface ã ãšæžãåºããªã â¡ ããããã£ã®åããŠããªã³åã ãšæžãåºããªã ⢠ããããã£ã®åããªãã©ã«åã ãšæžãåºããªã â ã«ã€ããŠã¯ããããã£ã Dto 䜿ããŸãããããã§è¯ããã§ããåŸè
ã«ã€ããŠã¯ãã³ã¬ãŒã¿ã䜿ã£ãŠåå®çŸ©ãæç€ºããŠäžããå¿
èŠããããŸãã 以äžã®ãããªãŠãŒããªãã£ãçšæããŠãããšäŸ¿å©ã§ãã /** * @example ApiProperty({ * description: '説æ', * ...enumSchema<Gender>(['male', 'female']) * }) */ export const enumSchema = < T extends string >( enums: readonly T [] , example?: T ) => { return { example: example ?? enums [ 0 ] , enum : enums as T [] , } satisfies SchemaObject ; } ; /** * @example ApiProperty({ * description: '説æ', * ...unionSchema([SomeDto, Some2Dto], exampleData) * }) */ export const unionSchema = < TargetDtoClass , const T extends ReadonlyArray < AbstractClass < TargetDtoClass >>, U = T [ number ] >( unions: T , example: U extends { prototype: unknown } ? U [ "prototype" ] : never ) => { return { example , oneOf: unions.map (( union ) => ( { $ref: getSchemaPath ( union ), } )), } satisfies SchemaObject ; } ; /** * @example ApiProperty({ * description: '説æ', * ...arraySchema(enumSchema(...args)) * }) */ export const arraySchema = ( itemSchema: SchemaObject ) => ( { type : "array" , items: itemSchema , } ); ãŠããªã³åããªãã©ã«å(+ãããã䜿ã£ãé
åå)ã®å Žåã«ã¯ type Gender = "male" | "female" class SomeDto { @ApiDecorator ( { ...enumSchema < Gender >( "male" ), } ) gender ! : Gender @ApiDecorator ( { ...unionSchema ( [ SomeDto , SomeDto2 ] , exampleData ), } ) someUnion ! : SomeDto | SomeDto2 @ApiDecorator ( { ...arraySchema ( enumSchema < Gender >( "male" )), } ) genders ! : Gender [] } ã®ããã«å®£èšããããšã§é©å㪠OAS ãåãåºãããšãã§ããŸãã å®è£
åŸã®èª²é¡æ ãŸã å®è£
ãé²ããŠããã¹ããŒã¿ã¹ã§ãããçŸæç¹ã§ã®ææãæ¯ãè¿ããããŠè¡ããŸãã Usecase åŒã³åºãã Fat ã«ãªããã¡ã§ã€ãã äž»ãªå¯äœçšåé¢ã®ãã¿ãŒã³ãšã㊠å¯äœçšã®ãµã³ãã€ãã å¯äœçšã DI ãããã¿ãŒã³ ãæ³å®ããŠããŸããããå¯äœçšåé¢ãæèããªããšãã®æžãæ¹ãšè¿ããŠæžããããã£ãããšãããã1 ãã 2 ã®ãã¿ãŒã³ã§ã®å®è£
ãå€ããªããã¡ã§ããã 2 ã®ãã¿ãŒã³ã§ã¯äŸåãã察象ãå€ããš DI ã®åŒã³åºãéšåã Fat ã«ãªãããã someUsecase ( { repository1 , repository2 , repository3 , // ... } )( arg ) ã®ãããªæžãæ¹ã«ãªããŸãã åé·ã§ã¯ãããŸããããããããªãéšåã§ã¯ããã®ã§èš±å®¹ããŠããŸãããã ããDI ãå¿
èŠãªãã±ãŒã¹ã§ãŸã§ DI ããŠããªããã¯æ³šæããŠãããããªãšæããŸãã ãŸãã颿°åDIã®ã©ã€ãã©ãªã§ãã velona ã䜿ããšãã¬ãŒã³ãªé«é颿°ã§ã¯ãªããå¿
èŠãªæã ã inject ããããšãã§ããã€ã³ã¿ãã§ãŒã¹ã«ãªã£ãŠããŸãã // ãªããžããªã®ãµã³ãã«ã®æç²ã§ã import { basicFn } from './' const injectedFn = basicFn.inject ( { add: ( a , b ) => a * b } ) expect ( injectedFn ( 2 , 3 , 4 )) .toBe ( 2 * 3 * 4 ) // pass expect ( basicFn ( 2 , 3 , 4 )) .toBe (( 2 + 3 ) * 4 ) // pass ããããã¢ãããŒãã§ããã°ãã¹ã以å€ã§äŸåãã颿°ãçšæããªããŠè¯ãã®ã§ Usecase å±€ãFatã«ãªããªãããããããã£ãã¢ãããŒããæ¡çšããŠããã°è¯ãã£ããªãšæã£ãŠããŸãã å€éš API ã«å¯Ÿããã¢ãã¯ãã€ãã ã¬ã€ã€ãŒæ§é ã®ç޹ä»ã«èŒããå³ã§ããããã®å³ãèŠãŠãããããã«ã³ã³ãããŒã©ãŒå±€ãããªããžããªå±€ã®å©çšã«ã¯ DI ãå©çšããŠããŸãããããã¯ã³ã³ãããŒã©ãŒå±€ã§ã¯ãå®éã®ããŒã¿ããŒã¹ã䜿ã£ããã¹ãããããã£ãããšãçç±ã§ãã äžæ¹ãããšå€éš API ã«äŸåããåŠçã«é¢ããŠã¯å®éã«ãªã¯ãšã¹ããéãã®ã§ã¯ãªãããã¹ãã±ãŒã¹ã«å¿ãã API ã¬ã¹ãã³ã¹ãžå·®ãæ¿ããå¿
èŠããããŸããDI ãå©çšããªãä»çµã¿ã¥ãããããŠããŸã£ãã®ã§ãå·®ãæ¿ããããã« Jest ã® mock ãå€çšãããŠãããèŸãç¶æ
ã«ãªã£ãŠããŸããŸããã msw çã䜿ã£ãŠãããã¯ãŒã¯ã®ã¬ã€ã€ãŒãã¢ãã¯ããããããªæ¹éãåãããã³ã³ãããŒã©ãŒå±€ããã DI ãã§ããä»çµã¿ãæŽåãã¹ãã ã£ããªãšããåŸæããããŸãã VSCode ã§ Go To Definition ã䜿ãã«ãã 颿°ã§ã® DI ã ãšãããã話ãªã®ããã§ãã const usecase = defineInjectable ( { fetchUser , } )(( deps ) => async () => { deps.fetchUser // <== ãã〠} ) ã®ãfetchUserãã®å®è£
ãžå®çŸ©ãžã£ã³ããããããŠãGo To DefinitionãããŠã DI 察象ã宣èšããŠãã 2 è¡ç®ã«ãžã£ã³ãããŠããŸããå®è£
ãèªã¿ã«ãããŸããã äŸåæ§é転ãããŠå®è£
ã§ã¯ãªãã€ã³ã¿ãã§ãŒã¹ã«äŸåããŠããã®ã§åœç¶ãšããã°åœç¶ã§ãããå®è£
éå§ããŠåœåã¯å°ãå°ããŸããã 察çãšããŠã¯ãã€ã³ã¿ãã§ãŒã¹ã«ãžã£ã³ãããã°è¯ãã®ã§ãGo To Type Definitionãã§ãžã£ã³ãããŠããããšå®è£
ã«é£ã¶ããšãã§ããŸãã åãšåäœãã¹ãã«ãããã£ãŒãããã¯ãé«éã§äœéšãè¯ã ãã¹ããå
ã«æžããŠå®è£
ãåŸããæžããŠãã TDD ã®éçºäœéšã¯ãå®éã« Web API ãåŒã³åºããŠè©Šããããå®è£
ããã£ãŠãããã®ãã£ãŒãããã¯ãéãããšããç¹ã§ãšãŠãéçºäœéšãè¯ãã§ãã åãåºæ¬çã«ã¯ãã¹ããšæ§é ãåãã ãšæã£ãŠã㊠ãã§ãã¯é床 ãã§ãã¯ã§ããç¯å² åãã§ã㯠ãšãŠãéã åã®ç¯å²ã®ãã§ã㯠DB äŸåãªããã¹ã éã ããžãã¯ã®æ€æ» DB äŸåãããã¹ã é
ã DB ã®åãå«ããŠå
æ¬çã« ã®ãããªç¹åŸŽããããŸã æè»ãªåãçšããŠé¢æ°ã®åŒæ°ã»æ»ãå€ã®åãæ£ç¢ºã«ã»å
ã«çšæããããã®ã§ãå
ã«ã€ã³ã¿ãã§ãŒã¹ããã¹ããçšæãã€ã€ãå®è£
ããªããåãã§ãã¯ãš DB éäŸåã®ãã¹ãã§ FB ãããããã®ã§ãšãŠãäœéšãè¯ãã£ãã§ãã ãŸãšã NestJS ã«ããã TS Backend èšèšã®äžäŸãš Tips ã玹ä»ããŸããã TypeScript ã®åã·ã¹ãã ãæŽ»ãããªãããåé§åã»é¢æ°åããã°ã©ãã³ã°ã®ãšãã»ã³ã¹ã軞㫠åã·ã¹ãã äžæã穎ã«ãªãããã NestJS ã®ãã€ã³ããé²ãæ¹æ³ å¯äœçšãã¬ã€ã€ãŒæ§é ã§åé¢ããŠãã¹ã¿ããªãã£ã»éçºäœéšãé«ããããããš æºæ£åžžç³»ã®äŸå€ã return ããç°åžžç³»ã®äŸå€ã throw ããããšã§å®è£
ãã·ã³ãã«ã«ä¿ã¡ã€ã€ãäŸå€ãåå®å
šã«æ±ããããš çã玹ä»ããŸããã éšåçã«ã§ã TS Backend èšèšã®åèã«ãªãã°å¹žãã§ãã èšèšã»å·çã®åèã«ããæç® Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F# by Scott Wlaschin TypeScript ã«ãã GraphQL ããã¯ãšã³ãéçº - Speaker Deck TypeScript ã®ç°åžžç³»è¡šçŸã®ããæãã®èœãšãæ | DevelopersIO Documentation | NestJS - A progressive Node.js framework Introduction - Mock Service Worker Docs no-restricted-imports - ESLint - Pluggable JavaScript Linter Prisma ã§æ¬ç©ã® DBMS ã䜿ã£ãŠèªåãã¹ããæžã - mizdra's blog *1 : ãããŸã§çè
ã®å人çãªè§£éã§ãã *2 : ããã§èšãEntityã¯O/R Mapperçãªæèã§ã¯ãªããã¯ãªãŒã³ã¢ãŒããã¯ãã£ã«ãããããã¡ã€ã³ã¢ãã«ãšçŽã¥ãããžãã¹ã«ãŒã«ãã«ãã»ã«ããããŠããã®ããšããæå³ã§ãã *3 : Pipe ã®èšå®ã¯å¿
èŠã§ãã *4 : ä»»æã®privateãªããããã£ãæã€BaseEntityãç¶æ¿ãããããšã§éã«ã¯ã©ã¹ã€ã³ã¹ã¿ã³ã¹ã«çµ±äžããããšããéžæè¢ãæããšæããŸãããã®ããæ¹ã ãšObject.assignãplainToClassãåå®å
šæ§ãæãªããããã«ãªã£ãŠããŸãããšãæ³å®ãããããåŒããŒã ã§ã¯é¿ããŠããŸãããããconstructorãã¡ãããšæžãããã«ãããã°åé·æ§ãšåŒãæãã«å¥å
šãªåœ¢ã§éçšã§ãããšæããŸãã *5 : äžè¬çã«ã¯ãã®ã³ã°çãå¯äœçšã«åé¡ãããŸãã *6 : å¯äœçšã¯åŒæ°ããã®ã³ãŒã«ããã¯é¢æ°ã«å«ãŸããŠããŠé¢æ°ã®è²¬åç¯å²ã«ã¯å¯äœçšããªãã»ãã¹ãã«ãããŠå¯äœçšããªãããšãæããŸãã