DEV Community

ByungJoon Lee
ByungJoon Lee

Posted on

jin-frame: Declarative, Type-Safe HTTP Requests in TypeScript

A Common Story

One day, a teammate reaches out:

“We’ve rolled out a new version of the API server. But the host has changed, so please update your client.”

Suddenly, your mind goes blank. You’ve hardcoded https://old-api.example.com all over the place.

Now you’ll have to track down and replace every instance.

Another day, a data architect says:

“For APIs a, b, and c, we need logging whenever they fail. Right now we have no logs, and it’s impossible to trace errors.”

The problem? These APIs are called in dozens of places.

Adding error logging to every call would be a painful refactor.

Sound familiar?

This is exactly why I built jin-frame.

Why jin-frame?

jin-frame isn’t just another Axios wrapper.

It lets you define HTTP requests declaratively and type-safely using classes and decorators.

  • 🎩 Declarative API definitions with decorators (@Param, @Query, @Header, @Body, @ObjectBody)
  • ⛑️ Type safety enforced at compile-time
  • 🎢 Production-ready features: retry, timeout, hooks, file upload, mocking
  • 🏭 Axios ecosystem compatibility
  • 🎪 Extensible via inheritance, so you can centralize config like host, timeout, or hooks

Example: Inheritance for Clean API Structure

With jin-frame, inheritance solves the exact problems we saw earlier:

“Host changed everywhere” or “Add common logging logic” becomes a one-line fix.

Base Class with Common Config

import { randomUUID } from 'crypto';

@Get({
  host: 'https://pokeapi.co',
  timeout: 3_000,
})
class PokemonAPI extends JinFrame<PokeRes> {
  @Query()
  declare readonly tid: string;

  protected static override getDefaultValues() {
    return { tid: randomUUID() };
  }

  override async $_postHook(req, reply, debugInfo) {
    if (reply.status >= 400) {
      console.error(`[Error] ${req.url}`, reply.status);
      // add your logging ...
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Every Pokémon API request shares the same host/timeout
  • A tid field is automatically added for request tracing
  • A common postHook handles error logging

Child Class: Pokémon by Name

@Get({ path: '/api/v2/pokemon/:name' })
class PokemonByName extends PokemonAPI<IPokemonData> {
  @Param()
  declare readonly name: string;
}
Enter fullscreen mode Exit fullscreen mode
  • Inherits host, timeout, and hooks from the parent
  • Child only needs to define its own path and name param

Usage

const frame = PokemonByName.of(b => b.from({ name: 'pikachu' }));
const reply = await frame.execute();
Enter fullscreen mode Exit fullscreen mode

Now when the API host changes, you only update the parent class.

When common logging is required, update the parent hook—all children inherit it.

And if needed, child classes can override hooks for special cases.

Why This Matters

  • Centralized config: Manage host, timeout, and hooks in one place
  • Extensibility: Override only what you need in child classes
  • Traceability: Automatic fields (e.g. tid) for request tracking
  • Consistency: Every API request looks and behaves the same

Instead of scattering functions across your codebase, inheritance keeps things clean.

As projects grow, this structure dramatically reduces maintenance overhead.

Conclusion

jin-frame is built for developers who:

  • Are tired of repetitive, scattered HTTP request code
  • Need easy-to-apply features like retry, logging, and timeout
  • Want a request layer reusable across Next.js RSC and CSR

👉 Check out jin-frame

With jin-frame, you’ll never again panic over “The host changed, now what?”

Top comments (0)