Introducing backhooks: hooks for the backend (nodeJS)

elmatella

Mathieu Marteau

Posted on January 24, 2023

Introducing backhooks: hooks for the backend (nodeJS)

I wrote my first real open source project last week and I got 40 stars on Github, I'm so excited!

React has hooks, Vue has composables, but what does backend has?

When building a back-end app with NodeJS, we always struggle with context. How to inject dependencies etc...

NestJS came with a good solution and is now widely used for their strong typed dependency injection.

But! NestJS also relies on some specificities like decorators, that are not easy to work with in different environments.

Wouldn't you like to just write hooks for your application, and access the data you need when you need it?

Imagine this express app route:

import { useHeaders, hooksMiddleware } from '@backhooks/http'

app.use(hooksMiddleware())

app.use('/foo', (req, res) => {
  const headers = useHeaders() // <- this is a hook
  res.send(headers)
})
Enter fullscreen mode Exit fullscreen mode

As you see, the useHeaders function is a global, unified function that could work for multiple runtimes, not only nodeJS - expressJS apps.

This makes it really easy to write functions, that are really easy to unit test:

export function useAuthorizationHeader () {
  const headers = useHeaders()
  return headers['authorization']
}
Enter fullscreen mode Exit fullscreen mode

And the good news is that the library is entirely type safe!

Creating hooks

Creating hooks is really straightforward. You can create hooks that rely on other hooks, or create a hook that holds a state for a particular run context (a request in general in an http context)

Stateful hooks

Sometimes, you have the need to share data between hooks call during a request execution.

You could create a useCount() that would increment at each call in a specific request:

import { createHook } from "@backhooks/core";

const [useCount, setCount] = createHook({
  // Here, we define a `data` function that will return
  // the initial state of the hook. This function must
  // be synchronous.
  data() {
    return {
      count: 0,
    };
  },
  // The `execute` function uses the state, can mutate the state
  // and returns a value. This function can be asynchronous.
  execute(state) {
    state.count++;
    return state.count;
  },
});
Enter fullscreen mode Exit fullscreen mode

That would result in the following:

app.get('/count', (req, res) => {
  console.log(useCount()) // Always 1
  console.log(useCount()) // Always 2
  console.log(useCount()) // Always 3
  res.send(useCount()) // Always sends 4
})
Enter fullscreen mode Exit fullscreen mode

You can also use the setCount function to override any state on the hook:

app.get('/count', (req, res) => {
  setCount({
    count: 50
  })
  console.log(useCount()) // Always 51
  console.log(useCount()) // Always 52
  console.log(useCount()) // Always 53
  ...
})
Enter fullscreen mode Exit fullscreen mode

Platform agnostic

You can use backhooks with any nodeJS application. Just run a context when you need it:

import { runHookContext } from '@backhooks/core'
import { useHeaders, configureHeadersHook } from '@backhooks/http'

runHookContext(async () => {
  setHeaders({
    headers: {
      authorization: 'Bearer abcd'
    }
  })
  const headers = useHeaders()
  console.log(headers.authorization) // Bearer abcd
})
Enter fullscreen mode Exit fullscreen mode

Each hook call must be run into a runHookContext callback in order to correctly attach hooks to the correct context.

The middleware provided for express or fastify actually does that automagically for each request.

So if you want to create a library, that relies on http requests informations, and want it to be compatible with every runtime, like express, fastify or even h3, you can just write hooks and it should work!

Imagine how easy it would be to write a useUser() function and be able to use it through all of your application?

Class injection

With backhooks, you can even do class injection really easily. That would be the equivalent of a request scoped provider for NestJS:

import { useLogger } from "./useLogger";

export class MyProvider {
  constructor(private readonly logger = useLogger());

  foo() {
    this.logger.debug("bar!"); // Type safe
  }
}
Enter fullscreen mode Exit fullscreen mode

and to register the hook:

import { createHook } from "@backhooks/core";
import { MyProvider } from "./MyProvider";

export const [useMyProvider] = createHook({
  data() {
    return new MyProvider();
  },
  execute(state) {
    return state;
  },
});
Enter fullscreen mode Exit fullscreen mode

And then use it anywhere, not just in other hooks function.. It's entirely plug and play!

app.use("/", (req, res) => {
  const myProvider = useMyProvider();
  myProvider.foo(); // Logs "bar!", while being type safe ;)
  res.send("ok!");
});
Enter fullscreen mode Exit fullscreen mode

If you like it, I invite you to leave a star on the repository on github to encourage me to go further with it, or comment something here!

https://github.com/coderhammer/backhooks

You can read the README of the repo if you want more informations and use case.

Cheers!

💖 💪 🙅 🚩
elmatella
Mathieu Marteau

Posted on January 24, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related