Skip to Content

TypeSafe Hooks for Custom Business Logic

Published: 2022-05-24

One of the strengths of WunderGraph is customization. With Hooks, you gain full control over the Request Lifecycle and can manipulate requests easily.

In comparison to other Frameworks, we're giving you the best possible Developer Experience for extensions

You can rewrite an incoming request and manipulate the Inputs of an Operation. Once the request was resolved, you're able to "hook" into the Request Lifecycle again and manipulate the response before sending it back to the client.

WunderGraph Hooks are similar to WebHooks, but TypeSafe, easy to test and easy to integrate into your WunderGraph applications.

So, how are WunderGraph Middleware Hooks different and what do we mean by TypeSafe?

Let's look at an example Operation using the SpaceX API:

query Missions ($find: MissionsFind) {
missions (find: $find) {
id
description
manufacturers
name
}
}

This Operation will generate a JSON RPC Endpoint as well as TypeScript models for both Inputs and the Response.

With Hooks, we're extending this with one addition, we're scaffolding a TypeSafe Hooks Configuration that allows you to write your Hooks using TypeScript. Here's how it looks like for the Operation above:

// all of this is generated
export interface HooksConfig {
queries?: {
Dragons?: {
preResolve?: (ctx: Context) => Promise<void>;
postResolve?: (ctx: Context, response: DragonsResponse) => Promise<void>;
mutatingPostResolve?: (ctx: Context, response: DragonsResponse) => Promise<DragonsResponse>;
};
Missions?: {
preResolve?: (ctx: Context, input: MissionsInput) => Promise<void>;
mutatingPreResolve?: (ctx: Context, input: MissionsInput) => Promise<MissionsInput>;
postResolve?: (ctx: Context, input: MissionsInput, response: MissionsResponse) => Promise<void>;
mutatingPostResolve?: (
ctx: Context,
input: MissionsInput,
response: MissionsResponse
) => Promise<MissionsResponse>;
};
};
mutations?: {};
}

You're able to define 4 distinct hooks, preResolve, mutatingPreResolve, postResolve and mutatingPostResolve. The pre- and postResolve hooks are good for logging or side effects, e.g. sending an E-Mail after a mutation. Mutating Hooks can be used to "mutate" the Request, e.g. change the input variables (preResolve) or modify the response (postResolve).

As you can see from the HooksConfig definition, all hooks are TypeSafe, inputs as well as the response type definitions. We automatically generate all of this from your Operations to give you the ultimate developer experience.

Finally, have a look at an example hook implementation. Hooks need to be implemented in the wundergraph.hooks.ts file.

// generated
const wunderGraphHooks = ConfigureWunderGraphHooks({
// generated
queries: {
// generated
Missions: {
// generated
async mutatingPreResolve (ctx, input) {
// user defined
return {
...input,
find: {
...input.find,
name: "Telstar"
}
}
}
}
}
})

In this case, we're hard-coding the find variable before the execution starts. Other examples would be to use the user object from the Context (ctx) to manipulate the input variables.

Here's another example of how to use a mutatingPostResolve hook to rewrite a response:

// generated
const wunderGraphHooks = ConfigureWunderGraphHooks({
// generated
queries: {
// generated
TowerByKey: {
// generated
async mutatingPostResolve(ctx, input, response){
// user defined
return {
...response,
data: {
...response.data,
TowerDetail: response.data?.TowerDetail?.map(detail => ({
...detail,
conductorSetHooks: detail.conductorSetHooks?.filter(csh=>csh.conductorSetHookId?.id !== "456"),
}))
}
}
}
}
}
});

What's best about all of this? It's TypeScript! It's using Node.JS to execute, you can use any npm package you want, write any code you want, whatever solves the problem best.

Additionally, it's possible to call all Operations (Queries & Mutations) from the hooks environment. This is useful, e.g. when you want to implement side effects after a successful user login. Here's an example:

// wundergraph.hooks.ts
const wunderGraphHooks = configureWunderGraphHooksWithClient(client => ({
authentication: {
postAuthentication: async user => {
if (!user.email || !user.name){
return
}
await client.mutations.UpsertLastLogin({
email: user.email,
name: user.name,
});
},
},
queries: {},
mutations: {},
}));

After the authentication is successful (postAuthentication), we upsert the user object in the database. No extra database client is required as the database is already accessible through the mutations.

Making all Operations accessible from the hooks environment simplifies app development a lot. In combination with the @internalOperation directive, it's possible to define Operations exclusively for internal purposes. This way, there's no need for an additional ORM or database client as the database is already accessible through Queries and Mutations. You stack becomes less complex, you have to deal with less complexity and your workflows are simplified.

How to#

If you're looking for more specific information on how to use Hooks, have a look at the reference.


Product

Comparisons

Subscribe to our newsletter!

Stay informed when great things happen! Get the latest news about APIs, GraphQL and more straight into your mailbox.

© 2022 WunderGraph