Exobase
  1. Getting Started
  2. Writing Hooks

There are three different types of hooks: root hooks, init hooks, and hooks. Depending on how you want to use your hook you’ll wire it up a bit differently.

All Hooks

Hooks are a functional pattern, they all share a similar pattern. The only difference is the last function, the args the hook accepts will change depending on the type.

type AnyHook = (options?) => (nextFunction) => (args) => Promise<any>

Root Hooks

A root hook is responsible for mapping/transforming framework specific arguments into the standard Props.

type RootHook = (options?) => (nextFunction) => (frameworkArgs) => Promise<any>

Here’s a simplified root hook, mostly stubbed, for the Next.js framework.

import type { Handler, Props, Request, Response } from '@exobase/core'
import { props, response } from '@exobase/core'
import type { NextApiRequest, NextApiResponse } from 'next'
import { try as tryit } from 'radash'

export type UseNextOptions = {}
export type NextFramework = {
  req: NextApiRequest
  res: NextApiResponse
}

export async function withNext(
  func: Handler,
  options: UseNextOptions,
  req: NextApiRequest,
  res: NextApiResponse
) {
  // For your hook, you'll implement this function
  // to generate the request for the framework your
  // working with.
  const request = createRequestForNext(req, res)
  // Call the next function in the composition
  // chain, using tryit to handle errors
  const [error, result] = await tryit(func)({
    // Exobase provides a props function to help init
    // the props object given the request.
    ...props(request),
    // Always add the raw framework arguments to
    // the props.framework property so downstream
    // functions have an escape hatch if needed.
    framework: { req, res }
  })
  // Exobase core provides some utility functions
  // to help convert an error or any function result
  // to the standard Response object with all info
  // like headers and status.
  const response = response(error, result)
  // For your hook, you'll implement this function
  // to apply (or in some frameworks: return) the
  // response to the framework.
  applyNextResponse(res, response)
}

export const useNext: (
  options?: UseNextOptions
) => (
  func: Handler
) => (req: NextApiRequest, res: NextApiResponse) => Promise<any> =
  options => func => (req, res) =>
    withNext(func, options ?? {}, req, res)

Init Hooks

Init hooks are a special type of hook that runs before the root hook. Typically we use this when we want some sort of side effect on the environment to prepare it for handling the request.

type InitHook = (options?) => (nextFunc) => (...args: any[]) => Promise<any>

Init hooks don’t make any assumption about the input arguments. Because they run before the root hook they don’t work with Props either.

Here’s a simplified init hook, mostly stubbed, that checks a local global cache for values and populates them if they don’t exist. In this example specifically, we’re getting secrets from AWS. The assumption is that downstream functions expect those values to be present.

import type { Handler, Props, Request, Response } from '@exobase/core'
import { props, response } from '@exobase/core'
import { try as tryit } from 'radash'

export type UseAWSSecretsOptions = Record<string, string>

declare global {
  var _useAWSSecretsCache: {}
}

export async function withAWSSecrets(
  func: Handler,
  options: UseAWSSecretsOptions,
  args: any[]
) {
  // Here you can do the logic to pull the secret
  // from AWS, maybe using the AWS SDK.
  // Check if the values already exist, if they do
  // you can immediatly call the next function.
  // If the values don't exist in the cache you'll
  // have to get them from AWS before calling the
  // next func.
  return await func(...args)
}

export const useAWSSecrets: (
  cacheName: string,
  /**
   * A map of the secrets to pull from AWS and
   * put into the cache where the key is the name
   * to use in the cache and the value is the path
   * to the secret in AWS.
   */
  secrets: Record<string, string>
) => (func: Handler) => (...args: any[]) => Promise<any> =
  options =>
  func =>
  (...args) =>
    withAWSSecrets(func, options ?? {}, args)

An example usage might be:

compose(
  useAWSSecrets('mySecrets', {
    databasePassword: '/database/password'
  }),
  useExpress(),
  async props => {
    console.log(globalThis.mySecrets.databasePassword) // => the password
  }
)

Hooks

A standard hook expects Props as input.

type Hook = (options?) => (func) => (props) => Promise<any>

Here’s an example of a hook meant for tracking an analytic event when an endpoint is used.

import type { Handler, Props, Request, Response } from '@exobase/core'
import { props, response } from '@exobase/core'
import { tryit, isFunction } from 'radash'

// Just helpful types that we'll use
// to keep strong typing for our
// hook function
export type Properties = Record<string, string | number>
export type Analytics = {
  track: (
    event: string,
    properties?: Record<string, string | number>
  ) => Promise<void>
}
export type UseTrackUsageOptions = {
  analytics: Analytics | (props: Props) => Analytics | Promise<Analytics>
  properties?: Properties | (props: Props) => Properties
}

// This is the real meat and body of
// the hook function, where we'll
// implement our hooks logic.
export async function withTrackUsage(
  func: Handler,
  event: string,
  options: UseTrackUsageOptions,
  props: Props
) {
  const analytics = await Promise.resolve(
    isFunction(options.analytics) ? options.analytics(props) : options.analytics
  )
  const properties = isFunction(options.properties)
    ? options.properties(props)
    : options.properties
  const [err] = await tryit(analytics.track)(event, properties)
  if (err) {
    console.error(err)
  }
  return await func(...args)
}

// The (options) => (func) => (props) => {} can get a bit
// difficult to read and understand when it's all under
// a single function with the implementation. Here, we just
// split off the functional setup work and call the `with`
// function to do the logic for us.
export const useTrackUsage: (
  eventName: string,
  options?: UseTrackUsageOptions
) => (func: Handler) => (props: Props) => Promise<any> =
  options =>
  func =>
  (props) =>
    withTrackUsage(func, eventName, options ?? {}, props)

Example usage might be

compose(
  useLambda(),
  useTrackUsage('api.request.library.books.list', {
    analytics: segment,
    properties: props => ({
      headers: props.request.headers
    })
  }),
  listLibraryBooks
)