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 ...
}
}
}
- 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;
}
- Inherits host, timeout, and hooks from the parent
- Child only needs to define its own
path
andname
param
Usage
const frame = PokemonByName.of(b => b.from({ name: 'pikachu' }));
const reply = await frame.execute();
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
With jin-frame, you’ll never again panic over “The host changed, now what?”
Top comments (0)