Skip to content

imjuni/jin-frame

Repository files navigation

jin-frame

ts Download Status Github Star Github Issues NPM version License ci codecov code style: prettier

HTTP Request = TypeScript Class

A reusable, declarative, type-safe, and extendable HTTP request library built on native fetch.

brand

Why jin-frame?

  1. Declarative API Definition — HTTP requests as TypeScript classes with decorators
  2. Type Safety — discriminated union response (ok: true | false) with full TypeScript generics
  3. Retry, Hooks, File Upload, Timeout, and AbortSignal support
  4. Built on native fetch — no extra HTTP client dependency
  5. RFC 6570 URI Template path parameters ({param})
  6. Builder pattern with compile-time field completeness checking
  7. Inheritance-friendly — share host/auth in a base class, override path in subclasses
  8. Runtime URL override — change host, pathPrefix, or path per _execute() call

Table of Contents

Install

npm install jin-frame --save
yarn add jin-frame --save
pnpm add jin-frame --save

Version

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

Usage

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);
}

Decorators

Method decorators

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

Field decorators

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

Inheritance

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 Pattern

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()));

Pass / Fail Response

_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
}

Retry, Timeout

@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),
})

Authorization

@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

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,
});

Runtime URL Override

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}',
});

Naming Convention

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.

Requirements

  • Node.js >= 22
  • TypeScript >= 5.0
  • experimentalDecorators and emitDecoratorMetadata enabled in tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Documentation

Full documentation: https://imjuni.github.io/jin-frame/

License

This software is licensed under the MIT.

About

A reusable, declarative, type-safe, and extendable HTTP request library.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors