|
| 1 | +# Backhooks |
| 2 | + |
| 3 | +Backhooks is a new way to write backend applications by using global hooks scoped to a specific context. |
| 4 | + |
| 5 | +It can be very useful for an HTTP application, for writing reusable and easily testable code. |
| 6 | + |
| 7 | +## Get started |
| 8 | + |
| 9 | +### Install dependency |
| 10 | + |
| 11 | +``` |
| 12 | +npm install @backhooks/core |
| 13 | +``` |
| 14 | + |
| 15 | +### Write your first hook |
| 16 | + |
| 17 | +```ts |
| 18 | +import { createHook } from "@backhooks/core"; |
| 19 | + |
| 20 | +const [useCount, updateCountState] = createHook({ |
| 21 | + data() { |
| 22 | + return { |
| 23 | + count: 0, |
| 24 | + }; |
| 25 | + }, |
| 26 | + execute(state) { |
| 27 | + state.count++; |
| 28 | + return state.count; |
| 29 | + }, |
| 30 | +}); |
| 31 | +``` |
| 32 | + |
| 33 | +### Execute your hook from a context |
| 34 | + |
| 35 | +```ts |
| 36 | +import { runHookContext } from "@backhooks/core"; |
| 37 | + |
| 38 | +runHookContext(() => { |
| 39 | + console.log(useCount()); // 1 |
| 40 | + console.log(useCount()); // 2 |
| 41 | + console.log(useCount()); // 3 |
| 42 | +}); |
| 43 | + |
| 44 | +runHookContext(() => { |
| 45 | + console.log(useCount()); // 1 |
| 46 | + console.log(useCount()); // 2 |
| 47 | + console.log(useCount()); // 3 |
| 48 | +}); |
| 49 | +``` |
| 50 | + |
| 51 | +## Usage with HTTP frameworks |
| 52 | + |
| 53 | +### Usage with ExpressJS |
| 54 | + |
| 55 | +``` |
| 56 | +npm install @backhooks/http |
| 57 | +``` |
| 58 | + |
| 59 | +```ts |
| 60 | +import * as express from "express"; |
| 61 | +import { useHeaders, hooksMiddleware } from "@backhooks/http"; |
| 62 | + |
| 63 | +const app = express(); |
| 64 | + |
| 65 | +app.use(hooksMiddleware()); |
| 66 | + |
| 67 | +app.get("/", async (req, res) => { |
| 68 | + const headers = useHeaders(); |
| 69 | + res.send(headers); |
| 70 | +}); |
| 71 | + |
| 72 | +app.listen(3000, () => { |
| 73 | + console.log("Listening on http://localhost:3000"); |
| 74 | +}); |
| 75 | +``` |
| 76 | + |
| 77 | +### Usage with Fastify |
| 78 | + |
| 79 | +``` |
| 80 | +npm install @backhooks/http |
| 81 | +``` |
| 82 | + |
| 83 | +```ts |
| 84 | +import Fastify from "fastify"; |
| 85 | +import { hooksMiddleware, useHeaders } from "@backhooks/http"; |
| 86 | + |
| 87 | +const fastify = Fastify({ |
| 88 | + logger: true, |
| 89 | +}); |
| 90 | + |
| 91 | +fastify.addHook("preHandler", hooksMiddleware()); |
| 92 | + |
| 93 | +// Declare a route |
| 94 | +fastify.get("/", () => { |
| 95 | + const headers = useHeaders(); |
| 96 | + return headers; |
| 97 | +}); |
| 98 | + |
| 99 | +// Run the server! |
| 100 | +fastify.listen({ port: 3000 }, function (err, address) { |
| 101 | + if (err) { |
| 102 | + fastify.log.error(err); |
| 103 | + process.exit(1); |
| 104 | + } |
| 105 | + // Server is now listening on ${address} |
| 106 | +}); |
| 107 | +``` |
| 108 | + |
| 109 | +## Writing hooks |
| 110 | + |
| 111 | +Writing hooks is very straightforward. Each hook holds a `state` for a particular context. |
| 112 | + |
| 113 | +Let's try out to write a `useAuthorizationHeader` hook: |
| 114 | + |
| 115 | +```ts |
| 116 | +import { createHook } from "@backhooks/core"; |
| 117 | +import { useHeaders } from "@backhooks/http"; |
| 118 | + |
| 119 | +const [useAuthorizationHeader, updateAuthorizationHeaderHookState] = createHook( |
| 120 | + { |
| 121 | + data() { |
| 122 | + // This function is called the first time the hook |
| 123 | + // is used to create the hook state within a specific |
| 124 | + // context |
| 125 | + |
| 126 | + // Note that in that function, you can use other hooks like `useHeaders` |
| 127 | + const headers = useHeaders(); |
| 128 | + |
| 129 | + // this function MUST be synchronous. |
| 130 | + return { |
| 131 | + header: headers["authorization"], |
| 132 | + }; |
| 133 | + }, |
| 134 | + execute(state) { |
| 135 | + // This function is called every time the application |
| 136 | + // calls the hook. It must return the value for this hook |
| 137 | + |
| 138 | + // Note that you can also call other hooks in this function. |
| 139 | + |
| 140 | + // This function CAN be asynchronous. You will have to await |
| 141 | + // the response of the hook if you make it asynchronous. |
| 142 | + return state.header; |
| 143 | + }, |
| 144 | + } |
| 145 | +); |
| 146 | +``` |
| 147 | + |
| 148 | +Now, let's see how we could update our hook state during a context execution: |
| 149 | + |
| 150 | +```ts |
| 151 | +import { runHookContext } from "@backhooks/core"; |
| 152 | + |
| 153 | +runHookContext(() => { |
| 154 | + updateAuthorizationHeaderHookState((state) => { |
| 155 | + return { |
| 156 | + ...state, |
| 157 | + header: "abc", |
| 158 | + }; |
| 159 | + }); |
| 160 | + const authorizationHeader = useAuthorizationHeader(); |
| 161 | + expect(authorizationHeader).toBe("abc"); |
| 162 | +}); |
| 163 | +``` |
| 164 | + |
| 165 | +This makes it really easy to test our code. You can even test your hooks by leveraging third party hooks update state function. Let's see how we could test our new hook: |
| 166 | + |
| 167 | +``` |
| 168 | +import { runHookContext } from "@backhooks/core"; |
| 169 | +import { configureHeadersHook } from '@backhooks/http'; |
| 170 | +import { useAuthorizationHeader } from './hooks/useAuthorizationHeader' |
| 171 | +
|
| 172 | +test('it should return the authorization header', async () => { |
| 173 | + runHookContext(() => { |
| 174 | + configureHeadersHook(state => { |
| 175 | + return { |
| 176 | + ...state, |
| 177 | + headers: { |
| 178 | + authorization: 'def' |
| 179 | + } |
| 180 | + } |
| 181 | + }) |
| 182 | + const authorizationHeader = useAuthorizationHeader() |
| 183 | + expect(authorizationHeader).toBe('def') // true |
| 184 | + }) |
| 185 | +}) |
| 186 | +``` |
| 187 | + |
| 188 | +## Global context |
| 189 | + |
| 190 | +We have seen that for hooks to work, it should be run in a context. |
| 191 | + |
| 192 | +Note that there also is a global context in your application. So that you **can** but should not really use hooks outside of a context. |
| 193 | + |
| 194 | +## Applications |
| 195 | + |
| 196 | +Hooks can have a variety of applications that are yet to be discovered. Here are some examples: |
| 197 | + |
| 198 | +### Logger hook: To log a requestId for each log entry |
| 199 | + |
| 200 | +```ts |
| 201 | +export default function () { |
| 202 | + const logger = useLogger(); |
| 203 | + logger.debug("I'm a log entry"); // {"requestId": "abcd", "message": "I'm a log entry"} |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +### Authentication hooks: To retrieve the authenticated user during a function execution |
| 208 | + |
| 209 | +```ts |
| 210 | +export default function () { |
| 211 | + const user = await useUserOrThrow(); |
| 212 | + // Do something with the authenticated user |
| 213 | +} |
| 214 | +``` |
| 215 | + |
| 216 | +### Validation hooks: To validate body |
| 217 | + |
| 218 | +```ts |
| 219 | +export default function () { |
| 220 | + const zodSchema = z.object({ |
| 221 | + foo: z.string().min(3), |
| 222 | + }); |
| 223 | + |
| 224 | + const parsedBody = useValidatedBody(zodSchema); |
| 225 | + |
| 226 | + console.log(parsedBody.foo); // Type safe, returns the foo property of the body |
| 227 | +} |
| 228 | +``` |
| 229 | + |
| 230 | +## Contribute |
| 231 | + |
| 232 | +This is really, really early stage. Everything is subject to change. The best way to help me with that is just to communicate me in some way that you are interested in this. You can open an issue or join me on my completely empty Discord Server, I'll be happy to interact. |
0 commit comments