Overview & Lifecycle
In this section we’ll go over the entire lifecycle of using @hulla/api
in action and will graph out all it’s features.
The reading is structured in a way that you can follow along and understand the concepts as you go.
Initialization
The first step is to initialize the API. This is done by calling the api()
function, which returns an instance of the API SDK that you can use to create routers and it’s procedures (routes).
import { api } from '@hulla/api'
const a = api() // initialization
The api
function also takes arguments that help you further modify your sdk behaviour.
- Context - allows you to pass in a context object that can be useful in route definitions and calls.
- Custom methods 🧪 - special way of defining api routes, think of them as procedures with side-effects. Primarily useful for library authors that build integrations with
@hulla/api
.
Router definition
Once we have initialization our API SDK, we’re ready to define and export our router that will serve as a logical encapsulation of our API/Procedure routes.
Each router must take a router name, that’s useful for registering route names and deduplicating them under the hood to separate identical route names under different routers. Apart from that, you can pass the following properties to the router
- Routes - Procedures, Procedures with resolvers or the procedures created via custom methods (you can define custom methods in the previous initialization step)
- Interceptors - special middleware calls, that can mutate the arguments of procedures/custom methods, results of these routes or even their resolvers.
- Adapters 🧪 - special functions, that alter the entire behaviour of the router and extends it’s functionality. For example the @tanstack/query integration or the swr integration
Here’s a quick example of a router definition
// a.router / a.procedure is available from the initialization step above
export const usersAPI = a.router({
name: 'usersAPI',
routes: [
// this is a single route definition 👀
a.procedure('getAllUsers', () => db.from('users').select('*')),
a.procedure('getUserById', (id: string) => db.from('users').where('id', '==', id).select('*')),
// with resolver:
a.procedure('withResolver', someFunction, (sum, ctx) => sum + ctx.foo),
// or a custom method:
a.request.get('posts', (userId: string) => `/users/${userId}/posts`)
]
})
Or if you’re more of a visual learner, here’s what the code sample above would look in a graph. In the code sample, we didn’t specify a any interceptors or adapters for the sake of simplicity. But feel free to check out their docs sections if you want to find out more about them.
While we will not go super in-depth how procedures, interceptors and adapters work here (read their own docs section if you’re interested), it might be useful in terms of visualisations to show you how they are composed in relation to the color-coded fields in the graphs above.
Usage
Here’s how we’d use our defined router in action.
Standard procedure
Now that we’ve defined our router API, it’s time to see it in action. We’ll import our defined router and call it’s procedures (in this case getUserById
).
import { usersAPI } from '@/api/users'
const users = usersAPI.call('getUserById', '123') // Promise<User>
Procedure with resolver
In the following examples we’ll no longer show the router graph to free up more space (and trust me we’re gonna need it 😅), but consider it’s always there, as we import our router from it.
If we were to call our procedure with resolver, it would look like this instead
Procedure with resolver and interceptors
and finally, we’ll add all 3 types of interceptors to the mix. Also keep in mind you’re not obligated to use every single one of these interceptors. You can deifne all 3 or you can define exactly 0 of them, so picture those in graph that you only define in the router definition step
Adapters
Last but not least, you may have noticed we didn’t show adapters in any of our graphs. The reason why we can’t provide a specific graph for adapters is because we’re kind of in a unchartable territory (pun intended) as to how the library (adapter) author chooses for it to interract. They have freedom to return whatever they want as well as choose wether to run interceptors, resolvers or only the vanilla procedure function.
So you should consult the documentation for individual adapters as to how exactly they operate. There’s no graph that will fit all of them.