Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/jin-frame/docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const getThemeConfig = (_locale?: string) => {
text: 'Naming Convention',
link: `${locale}/method/naming-convention.md`,
},
{
text: 'URL Template',
link: `${locale}/method/url-template.md`,
},
{
text: 'Authorization',
link: `${locale}/method/authorization.md`,
Expand Down
16 changes: 9 additions & 7 deletions packages/jin-frame/docs/field/param.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ outline: deep

In **`jin-frame`**, when you declare a class field with the `@Param()` decorator, the field value is **bound to the URL path parameter** and included in the request.

> Path parameters use **RFC 6570 URI Template** syntax (`{variable}`, not `:variable`). See [URL Template](../method/url-template.md) for details.

## Quick Example

```ts
@Get({ host: 'https://api.example.com', path: '/users/:userId/posts/:postId' })
@Get({ host: 'https://api.example.com', path: '/users/{userId}/posts/{postId}' })
export class UserPostFrame extends JinFrame {
@Param() declare readonly userId: string;
@Param() declare readonly postId: number;
Expand Down Expand Up @@ -49,7 +51,7 @@ The types supported by `@Param()` and their serialized results are as follows:
Arrays without options are serialized as JSON-like strings.

```ts
@Get({ host: 'https://api.example.com', path: '/users/:tags' })
@Get({ host: 'https://api.example.com', path: '/users/{tags}' })
export class ArrayParamFrame extends JinFrame {
@Param() declare readonly tags?: string[];
}
Expand All @@ -63,7 +65,7 @@ await ArrayParamFrame.of({ tags: ['red', 'blue'] }).execute();
Use `@Param({ comma: true })` to serialize arrays as **comma-separated values**.

```ts
@Get({ host: 'https://api.example.com', path: '/users/:tags' })
@Get({ host: 'https://api.example.com', path: '/users/{tags}' })
export class CommaParamFrame extends JinFrame {
@Param({ comma: true })
declare readonly tags?: string[];
Expand All @@ -78,7 +80,7 @@ await CommaParamFrame.of({ tags: ['red', 'blue', 'green'] }).execute();
Use `@Param({ bit: { enable: true } })` to serialize numeric arrays as a **bitwise OR sum**.

```ts
@Get({ host: 'https://api.example.com', path: '/flags/:flags' })
@Get({ host: 'https://api.example.com', path: '/flags/{flags}' })
export class BitwiseParamFrame extends JinFrame {
@Param({ bit: { enable: true } })
declare readonly flags?: number[];
Expand All @@ -95,7 +97,7 @@ await BitwiseParamFrame.of({ flags: [1, 2, 4] }).execute();
All param values are URL-safe encoded.

```ts
@Get({ host: 'https://api.example.com', path: '/tags/:tag' })
@Get({ host: 'https://api.example.com', path: '/tags/{tag}' })
export class EncodedParamFrame extends JinFrame {
@Param() declare readonly tag: string;
}
Expand All @@ -109,7 +111,7 @@ await EncodedParamFrame.of({ tag: 'hello world & tea' }).execute();
Path parameters are always **required**. Missing values result in an **error** because the URL cannot be built.

```ts
@Get({ host: 'https://api.example.com', path: '/items/:id' })
@Get({ host: 'https://api.example.com', path: '/items/{id}' })
export class OptionalParamFrame extends JinFrame {
@Param() declare readonly id?: number;
}
Expand All @@ -121,7 +123,7 @@ await OptionalParamFrame.of({}).execute();
## Combining with Query & Header

```ts
@Get({ host: 'https://api.example.com', path: '/orgs/:orgId/users/:userId' })
@Get({ host: 'https://api.example.com', path: '/orgs/{orgId}/users/{userId}' })
export class ListUsersParamFrame extends JinFrame {
@Param() declare readonly orgId: string;
@Param() declare readonly userId: string;
Expand Down
6 changes: 3 additions & 3 deletions packages/jin-frame/docs/getting-to-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { randomUUID } from 'node:crypto';

@Get({
host: 'https://pokeapi.co',
path: '/api/v2/pokemon/:name',
path: '/api/v2/pokemon/{name}',
})
export class PokemonFrame extends JinFrame {
@Param()
Expand Down Expand Up @@ -78,7 +78,7 @@ class PokemonPagingFrame extends JinFrame {
@Timeout(2_000) // 2s timeout
@Get({
host: 'https://pokeapi.co',
path: '/api/v2/pokemon/:name',
path: '/api/v2/pokemon/{name}',
})
export class PokemonDetailFrame extends JinFrame {
@Param()
Expand All @@ -91,7 +91,7 @@ export class PokemonDetailFrame extends JinFrame {
When defining Frame classes in jin-frame, fields typically use `declare public readonly`.

```ts
@Get({ path: '/api/v2/pokemon/:name' })
@Get({ path: '/api/v2/pokemon/{name}' })
class PokemonFrame extends JinFrame {
@Param()
declare public readonly name: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/jin-frame/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ features:
details: Built on the native fetch API — no third-party HTTP client dependency.
- title: Path Parameter Support
icon: 🎪
details: Supports path parameter substitution via URLs, e.g., example.com/:id.
details: Supports path parameter substitution via URLs, e.g., example.com/{id}.
---
6 changes: 3 additions & 3 deletions packages/jin-frame/docs/ko/getting-to-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { randomUUID } from 'node:crypto';

@Get({
host: 'https://pokeapi.co',
path: '/api/v2/pokemon/:name',
path: '/api/v2/pokemon/{name}',
})
export class PokemonFrame extends JinFrame {
@Param()
Expand Down Expand Up @@ -76,7 +76,7 @@ class PokemonPagingFrame extends JinFrame {
@Timeout(2_000) // 타임아웃 2초
@Get({
host: 'https://pokeapi.co',
path: '/api/v2/pokemon/:name',
path: '/api/v2/pokemon/{name}',
})
export class PokemonDetailFrame extends JinFrame {
@Param()
Expand All @@ -89,7 +89,7 @@ export class PokemonDetailFrame extends JinFrame {
jin-frame에서 Frame 클래스를 정의할 때는 필드에 보통 다음과 같이 `declare public readonly`를 사용합니다.

```ts
@Get({ path: '/api/v2/pokemon/:name' })
@Get({ path: '/api/v2/pokemon/{name}' })
class PokemonFrame extends JinFrame {
@Param()
declare public readonly name: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/jin-frame/docs/ko/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ features:
details: 네이티브 fetch API 위에 구축 — 서드파티 HTTP 클라이언트 의존성 없음.
- title: Path Parameter Support
icon: 🎪
details: Supports path parameter substitution via URLs, e.g., example.com/:id.
details: Supports path parameter substitution via URLs, e.g., example.com/{id}.
---

6 changes: 3 additions & 3 deletions packages/jin-frame/docs/ko/method/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { BearerTokenProvider } from 'jin-frame/providers';
@Security(new BearerTokenProvider(), 'my-bearer-token')
@Get({
host: 'https://api.example.com',
path: '/user/:id',
path: '/user/{id}',
})
export class UserProfileFrame extends JinFrame {
@Param()
Expand Down Expand Up @@ -159,7 +159,7 @@ export class DataFrame extends JinFrame {}
```ts
@Security(new BearerTokenProvider('my-auth'))
@Authorization('user-token-12345')
@Get({ host: 'https://api.example.com', path: '/user/:id' })
@Get({ host: 'https://api.example.com', path: '/user/{id}' })
export class UserProfileFrame extends JinFrame {
@Param()
declare public readonly id: string;
Expand All @@ -170,7 +170,7 @@ export class UserProfileFrame extends JinFrame {

```ts
@Security(new BearerTokenProvider(), 'user-token-12345')
@Get({ host: 'https://api.example.com', path: '/user/:id' })
@Get({ host: 'https://api.example.com', path: '/user/{id}' })
export class UserProfileFrame extends JinFrame {
@Param()
declare public readonly id: string;
Expand Down
6 changes: 3 additions & 3 deletions packages/jin-frame/docs/ko/method/inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class PokemonAPI<PASS = unknown, FAIL = unknown> extends JinFrame<PASS, FAIL> {
### 자식 클래스 정의

```ts
@Get({ path: '/api/v2/pokemon/:name' })
@Get({ path: '/api/v2/pokemon/{name}' })
class PokemonByNameId extends PokemonAPI<IPokemonData> {
@Param()
public declare readonly name: string;
Expand Down Expand Up @@ -71,7 +71,7 @@ class PokeBaseFrame<P = unknown, F = unknown> extends JinFrame<P, F> {

@Retry({ max: 5, interval: 1000 }) // 재시도 설정 추가
@Timeout(10_000) // 타임아웃 변경 5,000 > 10,000
@Get({ path: '/api/v2/pokemon/:name' })
@Get({ path: '/api/v2/pokemon/{name}' })
class PokemonByNameId extends PokeBaseFrame<IPokemonData> {
@Param()
public declare readonly name: string;
Expand Down Expand Up @@ -123,7 +123,7 @@ Hook 함수명에 `_` 접두사가 붙는 이유는 모든 인스턴스 메서
### 자식 클래스 Hook 확장

```ts
@Get({ path: '/api/v2/pokemon/:name' })
@Get({ path: '/api/v2/pokemon/{name}' })
class PokemonByNameId extends PokemonAPI<IPokemonData> {
@Param()
public declare readonly name: string;
Expand Down
102 changes: 102 additions & 0 deletions packages/jin-frame/docs/ko/method/url-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
outline: deep
---

# URL Template (Path Parameter)

jin-frame는 path parameter에 **RFC 6570 URI Template** 문법을 사용합니다. 플레이스홀더는 `:variableName`이 아닌 `{variableName}` 형식으로 작성합니다.

> **스펙**: [RFC 6570 – URI Template](https://www.rfc-editor.org/rfc/rfc6570)
> **구현체**: [url-template](https://www.npmjs.com/package/url-template) (npm)

---

## 문법

```
/path/{variable}
```

| 방식 | 예시 | 비고 |
|------|------|------|
| ✅ RFC 6570 (jin-frame) | `/users/{id}` | 올바른 방식 |
| ❌ Express 방식 | `/users/:id` | 지원하지 않음 |

---

## 기본 사용법

메서드 데코레이터의 `path`에 `{variable}` 형식으로 작성합니다. 각 플레이스홀더는 `@Param()` 데코레이터가 붙은 필드와 이름이 일치해야 합니다.

```ts
import { Get, Param, JinFrame } from 'jin-frame';

@Get({
host: 'https://api.example.com',
path: '/users/{userId}/posts/{postId}',
})
class UserPostFrame extends JinFrame {
@Param() declare readonly userId: string;
@Param() declare readonly postId: number;
}

const frame = UserPostFrame.of({ userId: 'alice', postId: 42 });
// → GET https://api.example.com/users/alice/posts/42
```

---

## `host`와 `pathPrefix`에도 적용 가능

URI Template 확장은 `path`뿐만 아니라 `host`, `pathPrefix`에도 적용됩니다. 멀티 테넌트 또는 환경별 URL 분기에 유용합니다.

```ts
@Get({
host: 'https://{tenant}.api.example.com',
pathPrefix: '/v{version}',
path: '/users/{id}',
})
class GetUserFrame extends JinFrame {
@Param() declare readonly tenant: string;
@Param() declare readonly version: string;
@Param() declare readonly id: string;
}

const frame = GetUserFrame.of({ tenant: 'acme', version: '2', id: 'alice' });
// → GET https://acme.api.example.com/v2/users/alice
```

---

## 런타임 오버라이드

`host`, `pathPrefix`, `path`는 `_execute()` 호출 시 오버라이드할 수 있습니다. 오버라이드된 값에도 URI Template 확장이 적용됩니다.

```ts
const reply = await frame._execute({
host: 'https://staging.api.example.com',
path: '/users/{id}',
});
```

---

## 필드명 매칭

플레이스홀더 이름은 클래스 필드명과 정확히 일치해야 합니다(대소문자 구분). 매칭되는 `@Param()` 필드가 없으면 플레이스홀더가 치환되지 않아 요청이 실패합니다.

```ts
@Get({ path: '/users/{id}' })
class Frame extends JinFrame {
@Param() declare readonly id: string; // ✅ {id}와 매칭
// @Param() declare readonly Id: string; // ❌ {id} ≠ {Id}
}
```

---

## 참고 문서

- [RFC 6570 – URI Template](https://www.rfc-editor.org/rfc/rfc6570) — 전체 스펙
- [WHATWG URL Standard](https://url.spec.whatwg.org/) — URL 파싱 및 직렬화 규칙
- [@Param 데코레이터](../field/param.md) — 지원 타입 및 직렬화 옵션
2 changes: 1 addition & 1 deletion packages/jin-frame/docs/ko/method/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class ProductValidator extends BaseValidator<
@Validator(new ProductValidator()) // 검증기 등록
@Get({
host: 'https://api.shop.com',
path: '/products/:id',
path: '/products/{id}',
})
class GetProductFrame extends JinFrame<Product> {
@Param()
Expand Down
8 changes: 4 additions & 4 deletions packages/jin-frame/docs/ko/usage-method.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ outline: deep
@Timeout(10_000) // 10s timeout
@Get({
host: 'https://api.example.com',
path: '/orgs/:orgId/users',
path: '/orgs/{orgId}/users',
authorization: process.env.YOUR_AUTH_TOKEN,
})
export class ListUsersFrame extends JinFrame {
Expand Down Expand Up @@ -80,7 +80,7 @@ await frame.execute();
```ts
@Patch({
host: 'https://api.example.com',
path: '/users/:id',
path: '/users/{id}',
})
export class UpdateUserFrame extends JinFrame {
@Param()
Expand All @@ -92,7 +92,7 @@ export class UpdateUserFrame extends JinFrame {

@Delete({
host: 'https://api.example.com',
path: '/users/:id',
path: '/users/{id}',
})
export class DeleteUserFrame extends JinFrame {
@Param()
Expand All @@ -107,7 +107,7 @@ export class DeleteUserFrame extends JinFrame {
| 옵션 | 타입 | 설명 |
| ------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| `host` | `string` | 베이스 URL(프로토콜 포함). 예: `https://api.example.com` |
| `path` | `string` | 경로. `:id` 처럼 **Path Param** 플레이스홀더 지원 |
| `path` | `string` | 경로. `{id}` 처럼 **Path Param** 플레이스홀더 지원 (RFC 6570 URI Template) |
| `pathPrefix` | `string` | `/api` 와 같이 path에 추가할 prefix를 설정할 수 있습니다. |
| `timeout` | `number` | 요청 타임아웃(ms). 미설정 시 라이브러리 기본값 사용 |
| `retry` | `{ max: number; interval?: number }` \* | 재시도 설정. `max`는 최대 시도 횟수, `interval`은 시도 간 대기(ms) |
Expand Down
4 changes: 2 additions & 2 deletions packages/jin-frame/docs/ko/what-is-jin-frame.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
서드파티 HTTP 클라이언트 없이 네이티브 `fetch` API 위에 구축되어 있습니다.

- 🎪 **Path Parameter 지원**
`example.com/:id`와 같은 path parameter를 타입 안정성을 보장하며 치환할 수 있습니다.
`example.com/{id}`와 같은 path parameter를 타입 안정성을 보장하며 치환할 수 있습니다.

## 제공 기능

Expand Down Expand Up @@ -74,7 +74,7 @@ class PokemonPagingFrame extends JinFrame {

@Get({
host: 'https://pokeapi.co',
path: '/api/v2/pokemon/:name',
path: '/api/v2/pokemon/{name}',
timeout: 2_000, // 2초
retry: { max: 3, inteval: 1000 }, // 1초 간격으로 3회 재시도
})
Expand Down
6 changes: 3 additions & 3 deletions packages/jin-frame/docs/method/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { BearerTokenProvider } from 'jin-frame/providers';
@Security(new BearerTokenProvider(), 'my-bearer-token')
@Get({
host: 'https://api.example.com',
path: '/user/:id',
path: '/user/{id}',
})
export class UserProfileFrame extends JinFrame {
@Param()
Expand Down Expand Up @@ -159,7 +159,7 @@ export class DataFrame extends JinFrame {}
```ts
@Security(new BearerTokenProvider('my-auth'))
@Authorization('user-token-12345')
@Get({ host: 'https://api.example.com', path: '/user/:id' })
@Get({ host: 'https://api.example.com', path: '/user/{id}' })
export class UserProfileFrame extends JinFrame {
@Param()
declare public readonly id: string;
Expand All @@ -170,7 +170,7 @@ export class UserProfileFrame extends JinFrame {

```ts
@Security(new BearerTokenProvider(), 'user-token-12345')
@Get({ host: 'https://api.example.com', path: '/user/:id' })
@Get({ host: 'https://api.example.com', path: '/user/{id}' })
export class UserProfileFrame extends JinFrame {
@Param()
declare public readonly id: string;
Expand Down
Loading
Loading