jin-frame
v5.1.0
Published
Reusable HTTP API request definition library
Maintainers
Readme
jin-frame
HTTP Request = TypeScript Class
A reusable, declarative, type-safe, and extendable HTTP request library built on native fetch.
Why jin-frame?
- Declarative API Definition
- Type Safety
- 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
Table of Contents
- Install
- Usage
- Builder Pattern
- Pass / Fail Response
- Validation
- Retry, Timeout
- Authorization
- Custom validateStatus
- Requirements
- Example
- License
Install
npm install jin-frame --saveyarn add jin-frame --savepnpm add jin-frame --saveUsage
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();
console.log(reply.data);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
}Validation
Validators run after the response is received and set valid and $validated on the response object. Pass and fail responses are validated independently.
@Get({
host: 'https://pokeapi.co',
path: '/api/v2/pokemon/{name}',
validators: {
pass: new MyPassValidator(),
fail: new MyFailValidator(),
},
})
export class PokemonFrame extends JinFrame<Pokemon, ErrorBody> { ... }
const reply = await frame._execute();
console.log(reply.valid); // boolean — false if validator rejected
console.log(reply.$validated); // ValidationResult with detailsFail validators never throw JinValidationError. JinValidationError is only thrown when a pass validator rejects and its type is 'exception'.
Retry, Timeout
import { Get, Param, Query, Retry, Timeout, JinFrame } from 'jin-frame';
@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;
@Query()
declare public readonly tid: string;
}Authorization
import { Get, Param, Query, JinFrame } from 'jin-frame';
@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;
@Query()
declare public readonly tid: string;
}Custom validateStatus
validateStatus receives both ok (native fetch Response.ok) and the raw status code, giving full control over what counts as a successful response.
const reply = await frame._execute({
validateStatus: (ok, status) => ok || status === 304,
});The default (isValidateStatusDefault) simply returns ok.
Requirements
- Node.js >= 22
- TypeScript >= 5.0 (tested up to 6.0)
experimentalDecoratorsandemitDecoratorMetadataenabled intsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Example
You can find more examples in the examples directory.
License
This software is licensed under the MIT.
