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.

  1. Context - allows you to pass in a context object that can be useful in route definitions and calls.
  2. 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.
const a = api({( context, methods )})import {api} from @hulla/api InitializationContextCustom methods

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

  1. Routes - Procedures, Procedures with resolvers or the procedures created via custom methods (you can define custom methods in the previous initialization step)
  2. Interceptors - special middleware calls, that can mutate the arguments of procedures/custom methods, results of these routes or even their resolvers.
  3. 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.

const a = api({ context, methods })import { api } from @hulla/apiInitializationContextCustom methodsRouterRouter nameInterceptorsRoutesProcedure routeProcedure routeProcedure routeCustom method routeexport const usersAPI = a.router({ name: 'usersAPI', routes: [ a.procedure(...), a.procedure(...), a.procedure(...), a.request.get(...) ], interceptors, adapters,})Adapters

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>
RouterRouter nameInterceptorsRoutesProcedure routeProcedure routeProcedure routeCustom method routeexport const usersAPI = a.router({ name: 'usersAPI', routes: [ a.procedure(...), a.procedure(...), a.procedure(...), a.request.get(...) ], interceptors, adapters,})AdaptersPage / ComponentimportsCalls a procedureusersAPI.call('getUserById', '123')passed argsProcedure route getUserByIdreturns a value

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

Page / ComponentCalls a procedureusersAPI.call('getUserById', '123')passed argsProcedure route getUserByIdreturns a valuereturns the result asresolver argumentprocedureresultResolver functioncontextargs are included in context+ what's passed in initialization

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

Page / ComponentCalls a procedureusersAPI.call('getUserById', '123')passed argsProcedure route getUserByIdreturns a valueprocedureresultResolver functioncontext+ what's passed in initializationArgs intrerceptormutates args or runs middlewarefn interceptorcontextcontextmutates procedure result orruns middlewareresolver interceptorcontextmutates resolver result / runs middlewareargs are included in the contextreturns the result as resolver argument

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.

Page / ComponentCalls a procedureusersAPI.specificAdapter(...)Adaptereach adapter exports it's own methods, so .specificAdapterknows which adapter it will use (you can have multiple)something happens via the adapter(usually returns a value)

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.