Skip to content

EnvyW6567/cat-framework

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

72 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Cat Framework

npm version License: MIT Node.js Version TypeScript

Coverage

🎯 λ°μ½”λ ˆμ΄ν„° 기반의 ν˜„λŒ€μ μ΄κ³  선언적인 개발 방식

πŸ“¦ λ…μžμ μΈ μ˜μ‘΄μ„± μ£Όμž… μ»¨ν…Œμ΄λ„ˆλ‘œ μžλ™ μ˜μ‘΄μ„± μ£Όμž…μ„ ν†΅ν•œ κΉ”λ”ν•œ μ•„ν‚€ν…μ²˜

πŸ”§ Express μŠ€νƒ€μΌμ˜ 직관적이고 μœ μ—°ν•œ 미듀웨어 μ‹œμŠ€ν…œ


πŸƒ Spring-Like Node.js μ›Ή ν”„λ ˆμž„μ›Œν¬

Cat FrameworkλŠ” TypeScript λ°μ½”λ ˆμ΄ν„°μ˜ νž˜μ„ ν™œμš©ν•˜μ—¬ κΉ”λ”ν•˜κ³  직관적인 APIλ₯Ό μ œκ³΅ν•˜λŠ” μ°¨μ„ΈλŒ€ μ›Ή ν”„λ ˆμž„μ›Œν¬μž…λ‹ˆλ‹€. λ³΅μž‘ν•œ μ„€μ • 없이 κ°•λ ₯ν•œ μ˜μ‘΄μ„± μ£Όμž…μ„ μ§€μ›ν•©λ‹ˆλ‹€.

@Controller('/api/users')
export class UserController {
    @GetMapping('/')
    async getUsers(): Promise<HttpResponseEntity> {
        return new HttpResponseEntity({ users: await this.userService.findAll() });
    }
}

πŸ—οΈ Spring μŠ€νƒ€μΌ μ˜μ‘΄μ„± μ£Όμž…

μƒμ„±μž νŒŒλΌλ―Έν„°λ₯Ό ν†΅ν•œ 직관적인 μ˜μ‘΄μ„± μ£Όμž…μœΌλ‘œ Spring κ°œλ°œμžλ“€μ—κ²Œ μΉœμˆ™ν•œ μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.

@Service()
export class UserService {
    constructor(private readonly userRepository: UserRepository) {
    }

    async findAll() {
        return this.userRepository.findAll();
    }
}

@Controller('/users')
export class UserController {
    constructor(private readonly userService: UserService) {
    } // μžλ™ μ£Όμž…!

    @GetMapping('/')
    async getUsers(): Promise<HttpResponseEntity> {
        const users = await this.userService.findAll();
        return new HttpResponseEntity({ users });
    }
}

πŸ”„ λ‹€ν˜•μ„± 지원 μ˜μ‘΄μ„± μ£Όμž…

μΈν„°νŽ˜μ΄μŠ€ 기반 개발과 @Inject λ°μ½”λ ˆμ΄ν„°λ₯Ό 톡해 κ΅¬ν˜„μ²΄λ₯Ό 자유둭게 ꡐ체할 수 μžˆμŠ΅λ‹ˆλ‹€.

// μΈν„°νŽ˜μ΄μŠ€ μ •μ˜
interface UserRepository {
    findAll(): Promise<User[]>;

    findById(id: number): Promise<User>;
}

// λ‹€μ–‘ν•œ κ΅¬ν˜„μ²΄
@Repository("UserMysqlRepository")
export class UserMysqlRepository implements UserRepository {
    async findAll() { /* MySQL κ΅¬ν˜„ */
    }

    async findById(id: number) { /* MySQL κ΅¬ν˜„ */
    }
}

@Repository("UserMongoRepository")
export class UserMongoRepository implements UserRepository {
    async findAll() { /* MongoDB κ΅¬ν˜„ */
    }

    async findById(id: number) { /* MongoDB κ΅¬ν˜„ */
    }
}

// κ΅¬ν˜„μ²΄ 선택적 μ£Όμž…
@Service()
export class UserService {
    constructor(
        @Inject("UserMysqlRepository") private readonly userRepository: UserRepository
    ) {
    }

    // ν…ŒμŠ€νŠΈ ν™˜κ²½μ—μ„œλŠ” Mock μ£Όμž… κ°€λŠ₯
    // @Inject("UserMockRepository") private readonly userRepository: UserRepository
}

πŸ” μžλ™ 파일 μŠ€μΊλ‹

λ³΅μž‘ν•œ λͺ¨λ“ˆ λ“±λ‘μ΄λ‚˜ import 관리 없이, 파일 μŠ€μΊλ‹μ„ 톡해 μžλ™μœΌλ‘œ μ˜μ‘΄μ„±μ„ λ°œκ²¬ν•˜κ³  λ“±λ‘ν•©λ‹ˆλ‹€:

// 이런 λ³΅μž‘ν•œ 섀정은 ν•„μš” μ—†μŠ΅λ‹ˆλ‹€!
// app.module.ts ❌
// const app = new App([UserController, UserService, UserRepository]);

// κ·Έλƒ₯ λ°μ½”λ ˆμ΄ν„°λ§Œ 뢙이면 끝! βœ…
@Controller('/users')  // μžλ™ μŠ€μΊ” & 등둝
export class UserController {
...
}

@Service()            // μžλ™ μŠ€μΊ” & 등둝  
export class UserService {
...
}

🌱 μ„€μΉ˜

npm install @envyw/cat-framework reflect-metadata class-transformer class-validator

λ‹€λ₯Έ νŒ¨ν‚€μ§€ λ§€λ‹ˆμ €λ„ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

yarn add @envyw/cat-framework reflect-metadata class-transformer class-validator
pnpm add @envyw/cat-framework reflect-metadata class-transformer class-validator
bun add @envyw/cat-framework reflect-metadata class-transformer class-validator

πŸ—οΈ μ‚¬μš©λ²•

App.ts

미듀웨어 체이닝을 톡해 λΌμš°ν„°λ₯Ό λ“±λ‘ν•˜μ„Έμš”. 그리고 μ„œλ²„λ₯Ό μ‹€ν–‰ ν•¨μˆ˜λ₯Ό λ“±λ‘ν•˜μ„Έμš”.

import { CatServer, Injectable } from '@envyw/cat-framework';

@Injectable()
export class App {
    constructor(
        private readonly router: Router,
        private readonly server: CatServer,
    ) {
    }

    async start() {
        await this.configureServer()

        await this.server.create()
        await this.server.listen(3001)
    }

    private async configureServer() {
        this.server
            .use(this.router)
    }
}

main.ts

main μ§„μž…μ μ—μ„œ cat ν•¨μˆ˜λ‘œ App을 λ“±λ‘ν•˜κ³  μ‹€ν–‰ν•˜μ„Έμš”.

import { cat } from '@envyw/cat-framework';

cat(App).then(app => app.start())

μ‹€ν–‰

⚠️ typescript JIT transform λͺ¨λ“ˆμ„ μ‚¬μš©ν•΄μ„œ μ‹€ν–‰ν•΄μ•Όν•©λ‹ˆλ‹€. (보완 쀑)

$ ts-node main.ts
Server running on port 3000

🎯 컨트둀러 μ •μ˜

// user.controller.ts
import { Controller, GetMapping, PostMapping, RequestBody, HttpResponseEntity } from '@envyw/cat-framework';

@Controller('/users')
export class UserController {
    @GetMapping('/')
    async getUsers(): Promise<HttpResponseEntity> {
        return new HttpResponseEntity({ users: [] }, 200);
    }

    @PostMapping('/')
    async createUser(@RequestBody() body: any): Promise<HttpResponseEntity> {
        return new HttpResponseEntity({ message: 'μ‚¬μš©μžκ°€ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€', user: body }, 201);
    }
}

πŸ“¦ μ˜μ‘΄μ„± μ£Όμž…

// user.service.ts
import { Service } from '@envyw/cat-framework';

@Service()
export class UserService {
    async findAll() {
        return [{ id: 1, name: '홍길동' }];
    }
}

// user.controller.ts
@Controller('/users')
export class UserController {
    constructor(private readonly userService: UserService) {
    }

    @GetMapping('/')
    async getUsers(): Promise<HttpResponseEntity> {
        const users = await this.userService.findAll();
        return new HttpResponseEntity({ users }, 200);
    }
}

πŸ”§ 미듀웨어

import { Injectable, Middleware, HttpRequest, HttpResponse } from '@envyw/cat-framework';

@Injectable()
export class LoggerMiddleware implements Middleware {
    async handle(req: HttpRequest, res: HttpResponse, next: Function): Promise<void> {
        console.log(`${req.method} ${req.path}`);
        next();
    }
}

// 미듀웨어 등둝
@Injectable()
export class App {
    constructor(
        private readonly server: CatServer,
        private readonly loggerMiddleware: LoggerMiddleware
    ) {
    }

    async start() {
        this.server.use(this.loggerMiddleware);
        await this.server.create();
        await this.server.listen(3000);
    }
}

πŸ“ 파일 μ—…λ‘œλ“œ

@Controller('/upload')
export class UploadController {
    @PostMapping('/')
    async uploadFile(@Multipart() files: any[]): Promise<HttpResponseEntity> {
        return new HttpResponseEntity({
            message: '파일이 μ—…λ‘œλ“œλ˜μ—ˆμŠ΅λ‹ˆλ‹€',
            count: files.length
        });
    }
}

πŸ” 인증

@Controller('/protected')
export class ProtectedController {
    @GetMapping('/profile')
    async getProfile(@Authenticated() userId: number): Promise<HttpResponseEntity> {
        return new HttpResponseEntity({ userId, message: '인증된 μ‚¬μš©μž' });
    }
}

πŸ“– API λ¬Έμ„œ

λ°μ½”λ ˆμ΄ν„°

Cat FrameworkλŠ” λ‹€μŒ λ°μ½”λ ˆμ΄ν„°λ“€μ„ μ œκ³΅ν•©λ‹ˆλ‹€:

클래슀 λ°μ½”λ ˆμ΄ν„°

  • @Controller(basePath?) - 컨트둀러 클래슀 μ •μ˜
  • @Service() - μ„œλΉ„μŠ€ 클래슀 ν‘œμ‹œ
  • @Repository() - λ ˆν¬μ§€ν† λ¦¬ 클래슀 ν‘œμ‹œ
  • @Injectable() - μ£Όμž… κ°€λŠ₯ν•œ 클래슀 ν‘œμ‹œ

λ©”μ„œλ“œ λ°μ½”λ ˆμ΄ν„°

  • @GetMapping(path) - GET μš”μ²­ λΌμš°νŒ…
  • @PostMapping(path) - POST μš”μ²­ λΌμš°νŒ…

νŒŒλΌλ―Έν„° λ°μ½”λ ˆμ΄ν„°

  • @RequestBody() - μš”μ²­ λ³Έλ¬Έ μ£Όμž…
  • @RequestParam() - 쿼리 νŒŒλΌλ―Έν„° μ£Όμž…
  • @Multipart() - λ©€ν‹°νŒŒνŠΈ 파일 μ£Όμž…
  • @Authenticated() - 인증된 μ‚¬μš©μž ID μ£Όμž…
  • @Inject(token) - μ»€μŠ€ν…€ μ˜μ‘΄μ„± μ£Όμž…

핡심 클래슀

HttpResponseEntity 응닡 데이터 λž˜ν•‘ μ—”ν„°ν‹°

const response = new HttpResponseEntity(body, status, headers);

κΈ°λ³Έκ°’: body = {}, status = 200, headers = {}

Middleware 미듀웨어 μΈν„°νŽ˜μ΄μŠ€

interface Middleware {
    handle(req: HttpRequest, res: HttpResponse, next: Function, err?: Error): Promise<void>
}

μ„€μ • μ˜΅μ…˜

ν™˜κ²½ λ³€μˆ˜

Cat FrameworkλŠ” λ‹€μŒ ν™˜κ²½ λ³€μˆ˜λ“€μ„ μ§€μ›ν•©λ‹ˆλ‹€.

  • CAT_LOG_LEVEL - λ‘œκΉ… 레벨 μ„€μ • (ERROR, WARN, INFO, DEBUG)
  • VIEW_FILE_PATH - HTML 파일 경둜
  • STATIC_FILE_PATH - 정적 파일 경둜

TypeScript μ„€μ •

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true
  }
}

❓ FAQ

μ»¨νŠΈλ‘€λŸ¬κ°€ λ“±λ‘λ˜μ§€ μ•Šμ•„μš”

κ°€μž₯ κ°€λŠ₯성이 높은 원인은 컨트둀러 파일이 μ˜¬λ°”λ₯Έ μœ„μΉ˜μ— μžˆμ§€ μ•ŠκΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. src/ 디렉토리 내에 μžˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”.

디버그 λͺ¨λ“œλ₯Ό 켜고 λ‹€μ‹œ μ‹œλ„ν•΄λ³΄μ„Έμš”:

CAT_LOG_LEVEL=DEBUG node main.js

μ½˜μ†”μ— 도움이 λ˜λŠ” 였λ₯˜ λ©”μ‹œμ§€κ°€ 좜λ ₯λ©λ‹ˆλ‹€.

미듀웨어가 μ‹€ν–‰λ˜μ§€ μ•Šμ•„μš”

미듀웨어가 server.create() 전에 λ“±λ‘λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”:

async
start()
{
    this.server.use(this.middleware); // create() 전에 등둝
    await this.server.create();
    await this.server.listen(3000);
}

μš”μ²­ 본문을 읽을 수 μ—†μ–΄μš”

Content-Type 헀더가 application/json으둜 μ„€μ •λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”. Cat FrameworkλŠ” ν˜„μž¬ JSON ν˜•νƒœμ˜ μš”μ²­ 본문만 μ§€μ›ν•©λ‹ˆλ‹€.

TypeScriptμ—μ„œ νƒ€μž… 였λ₯˜κ°€ λ°œμƒν•΄μš”

reflect-metadataλ₯Ό importν–ˆλŠ”μ§€ ν™•μΈν•˜κ³ , tsconfig.jsonμ—μ„œ λ°μ½”λ ˆμ΄ν„° 섀정이 ν™œμ„±ν™”λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

정적 νŒŒμΌμ„ μ œκ³΅ν•  수 μžˆλ‚˜μš”?

λ„€! ν™˜κ²½ λ³€μˆ˜λ₯Ό μ„€μ •ν•˜μ„Έμš”:

STATIC_FILE_PATH=./public
VIEW_FILE_PATH=./views

이제 정적 νŒŒμΌλ“€μ΄ μžλ™μœΌλ‘œ μ œκ³΅λ©λ‹ˆλ‹€.

μ˜μ‘΄μ„±

  • reflect-metadata - λ°μ½”λ ˆμ΄ν„° 메타데이터 (ν•„μˆ˜)
  • class-transformer - DTO λ³€ν™˜
  • class-validator - μš”μ²­ 검증

About

spring friendly node.js http-server framework

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors