When working with REST APIs, having a clean, expressive client can greatly simplify how you write code. Imagine calling your API like this:
const api = createApi('localhost');
api.users.get().then(console.log);
api.posts.post({ title: 'Hello' }).then(console.log);
This syntax is not only clean but reads almost like a natural language. The secret behind this elegant interface? It’s all about dynamic property access powered by JavaScript’s Proxy object.
- Resources like
usersandpostsare not predefined properties of theapiobject. - HTTP methods like
getandpostare also dynamically resolved.
This means you don’t have to write boilerplate code to explicitly declare every resource or method. Instead, these identifiers are resolved at runtime!
The core enabler is Proxy
A Proxy lets you intercept fundamental operations on objects, such as property access or function calls, and define custom behaviors.
Here’s the minimal implementation of createApi:
const createApi = (domain, path = '') => {
return new Proxy(() => {}, {
get(target, key) {
return createApi(domain, `${path}/${key.toString()}`);
},
apply(target, thisArg, args) {
const i = path.lastIndexOf('/');
const method = path.slice(i + 1).toUpperCase();
const url =
domain + (i === -1 ? '/' : path.slice(0, i));
const options = {
method,
headers: {
'Content-Type': 'application/json',
},
};
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) && args[0]) {
options.body = JSON.stringify(args[0]);
}
return fetch(url, options).then((res) => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
});
},
});
};
const api = createApi('localhost');
api.users.get().then(console.log);
api.posts.post({ title: 'Hello' }).then(console.log);
How It Works: get and apply traps explained
The get Trap — Capturing Property Access
- Every time you access a property on the proxy — like
api.usersorapi.posts— thegettrap intercepts that access. - Instead of returning a fixed value, it calls
createApirecursively, appending the accessed property name (users,posts, or HTTP methods likeget,post, etc.) to the URL path argument. - This recursive design enables infinitely deep property chains without needing to define them explicitly upfront.
- The recursion ends when the proxy is invoked as a function (via the
applytrap), which triggers the actual HTTP request.
The apply Trap — Handling Function Calls
- When you finally call the proxy as a function —
api.users.get()—theapplytrap intercepts the call. - It extracts the HTTP method from the last part of the path (like
get,post, etc.), builds the full URL, and executes the HTTP request usingfetch.
By combining the get trap for dynamic property access, the apply trap for function invocation, and recursion for building URL paths, this approach enables the creation of a powerful, flexible, and elegant API client.
Note: In JavaScript, a trap is a special method defined in a
Proxyhandler object. These traps intercept fundamental operations—like property access, function calls, or property assignments—allowing you to customize how those operations behave.
This post was inspired by the API design found in elysiajs/eden.
Top comments (0)