TypeSafe Hooks for Custom Business Logic
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) {iddescriptionmanufacturersname}}
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 generatedexport 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.
// generatedconst wunderGraphHooks = ConfigureWunderGraphHooks({// generatedqueries: {// generatedMissions: {// generatedasync mutatingPreResolve (ctx, input) {// user definedreturn {...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:
// generatedconst wunderGraphHooks = ConfigureWunderGraphHooks({// generatedqueries: {// generatedTowerByKey: {// generatedasync mutatingPostResolve(ctx, input, response){// user definedreturn {...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.tsconst 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.