HTTP Request = TypeScript Class
A reusable, declarative, type-safe, and extendable HTTP request library built on native fetch.
Why jin-frame?
- Declarative API Definition — HTTP requests as TypeScript classes with decorators
- Type Safety — discriminated union response (
ok: true | false) with full TypeScript generics - Retry, Hooks, File Upload, Timeout, and AbortSignal support
- Built on native
fetch— no extra HTTP client dependency - RFC 6570 URI Template path parameters (
{param}) - Builder pattern with compile-time field completeness checking
- Inheritance-friendly — share host/auth in a base class, override path in subclasses
- Runtime URL override — change host, pathPrefix, or path per
_execute()call
- Install
- Version
- Usage
- Decorators
- Inheritance
- Builder Pattern
- Pass / Fail Response
- Retry, Timeout
- Authorization
- validateStatus
- Runtime URL Override
- Naming Convention
- Requirements
- Documentation
- License
npm install jin-frame --saveyarn add jin-frame --savepnpm add jin-frame --save| Version | HTTP Client | Notes |
|---|---|---|
| < 5.0 | Axios | Requires axios as a peer dependency |
| >= 5.0 | native fetch |
No HTTP client dependency; Node.js >= 22 required |
import { Get, Param, Query, JinFrame } from 'jin-frame';
import { randomUUID } from 'node:crypto';
@Get({
host: 'https://pokeapi.co',
path: '/api/v2/pokemon/{name}',
})
export class PokemonFrame extends JinFrame {
@Param()
declare public readonly name: string;
@Query()
declare public readonly tid: string;
}
const frame = PokemonFrame.of({ name: 'pikachu', tid: randomUUID() });
const reply = await frame._execute();
if (reply.ok) {
console.log(reply.data);
}| Decorator | Description |
|---|---|
@Get |
HTTP GET |
@Post |
HTTP POST |
@Put |
HTTP PUT |
@Patch |
HTTP PATCH |
@Delete |
HTTP DELETE |
@Head |
HTTP HEAD |
@Options |
HTTP OPTIONS |
@Retry |
Retry configuration |
@Timeout |
Request timeout |
@Dedupe |
Deduplicate concurrent identical requests |
@Security |
Security provider for authentication |
@Validator |
Response validators |
| Decorator | Mapped to |
|---|---|
@Param |
URL path parameter ({name}) |
@Query |
URL query string |
@Body |
Request body field |
@ObjectBody |
Request body (entire object merged) |
@Header |
Request header |
@Cookie |
Cookie request header |
Define shared settings (host, auth, pathPrefix, default field values) in a base class and override only the path in each subclass.
import { Get, Post, Param, Query, Header, Body, JinFrame } from 'jin-frame';
import { randomUUID } from 'node:crypto';
@Get({ host: 'https://pokeapi.co', pathPrefix: '/api/v2' })
class PokeApiFrame extends JinFrame {
@Header({ replaceAt: 'X-Request-Id' })
declare public readonly requestId: string;
protected static override getDefaultValues() {
return { requestId: randomUUID() };
}
}
@Get({ path: '/pokemon/{name}' })
class GetPokemonFrame extends PokeApiFrame {
@Param()
declare public readonly name: string;
}
@Post({ path: '/pokemon' })
class CreatePokemonFrame extends PokeApiFrame {
@Body()
declare public readonly name: string;
}
// requestId is filled automatically by getDefaultValues
const frame = GetPokemonFrame.of({ name: 'pikachu' });
const reply = await frame._execute();builder() tracks which fields have been set at the type level. build() is only available once all public fields are assigned, catching missing fields at compile time.
const frame = PokemonFrame.builder()
.set('name', 'pikachu')
.set('tid', randomUUID())
.build(); // compile error if any public field is missing
const reply = await frame._execute();of() also accepts a builder callback:
const frame = PokemonFrame.of((b) => b.set('name', 'pikachu').set('tid', randomUUID()));_execute() returns a discriminated union typed by ok:
const reply = await frame._execute<MyFrame, Pokemon, ErrorBody>();
if (reply.ok) {
console.log(reply.data); // typed as Pokemon
} else {
console.error(reply.data); // typed as ErrorBody
}@Timeout(2000)
@Retry({ max: 5, interval: 1000 })
@Get({
host: 'https://pokeapi.co',
path: '/api/v2/pokemon/{name}',
})
export class PokemonFrame extends JinFrame {
@Param()
declare public readonly name: string;
}getInterval supports exponential backoff:
@Retry({
max: 5,
getInterval: (retry) => Math.min(1000 * 2 ** retry, 30_000),
})@Get({
host: 'https://pokeapi.co',
path: '/api/v2/pokemon/{name}',
authorization: process.env.YOUR_KEY_HERE,
})
export class PokemonFrame extends JinFrame {
@Param()
declare public readonly name: string;
}validateStatus can be set at the decorator level or overridden per _execute() call:
// decorator-level default
@Get({
host: 'https://pokeapi.co',
path: '/api/v2/pokemon/{name}',
validateStatus: (ok, status) => ok || status === 404,
})
export class PokemonFrame extends JinFrame { ... }
// _execute()-level override (takes precedence)
const reply = await frame._execute({
validateStatus: (ok, status) => ok || status === 304,
});host, pathPrefix, and path can be overridden per _execute() call:
const reply = await frame._execute({
host: 'https://staging.api.example.com',
pathPrefix: '/v3',
path: '/pokemon/{name}',
});| Prefix | Used for |
|---|---|
# |
Internal state (JavaScript private fields) — invisible to subclasses |
_ |
All instance methods — public API, hooks, and helpers |
| (none) | Static methods |
See the full Naming Convention documentation for details.
- Node.js >= 22
- TypeScript >= 5.0
experimentalDecoratorsandemitDecoratorMetadataenabled intsconfig.json
Full documentation: https://imjuni.github.io/jin-frame/
- Getting Started
- Inheritance
- Builder Pattern
- URL Template (RFC 6570)
- Form / File Upload
- Retry
- Authorization
- Validation
- Naming Convention
This software is licensed under the MIT.
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }