Appearance
Using Middleware for Dependency Injection
Inngest Functions running in the same application often need to share common clients instances such as database clients or third-party libraries.
The following is an example of adding a OpenAI client to all Inngest functions, allowing them immediate access without needing to create the client themselves.
We can use the dependencyInjectionMiddleware to add arguments to a function's input.
Check out the TypeScript example for a customized middleware.
ts
import { dependencyInjectionMiddleware } from "inngest";
import OpenAI from 'openai';
const openai = new OpenAI();
const inngest = new Inngest({
id: 'my-app',
middleware: [
dependencyInjectionMiddleware({ openai }),
],
});Our Inngest Functions can now access the OpenAI client through the context:
ts
inngest.createFunction(
{ name: "user-create", triggers: { event: "app/user.create" } },
async ({ openai }) => {
const chatCompletion = await openai.chat.completions.create({
messages: [{ role: "user", content: "Say this is a test" }],
model: "gpt-3.5-turbo",
});
// ...
},
);Callout: 💡 Types are inferred from middleware outputs, so your Inngest functions will see an appropriately-typed openai property in their input.
Explore other examples in the TypeScript SDK Middleware examples page.
Advanced mutation
When the middleware runs, the types and data within the passed ctx are merged on top of the default provided by the library. This means that you can use a few tricks to overwrite data and types safely and more accurately.
For example, here we use a const assertion to infer the literal value of our foo example above.
ts
// In middleware
dependencyInjectionMiddleware({
foo: "bar",
} as const)
// In a function
async ({ event, foo }) => {
// ^? (parameter) foo: "bar"
}Ordering middleware and types
Middleware runs in the order specified when registering it (see Middleware lifecycle), which affects typing too.
When inferring a mutated input or output, the SDK will apply changes from each middleware in sequence, just as it will at runtime. This means that for two middlewares that add a foo value to input arguments, the last one to run will be what it seen both in types and at runtime.
Our custom OpenAIMiddleware relies on the transformFunctionInput hook to mutate the function's context:
ts
import { Middleware } from "inngest";
import OpenAI from "openai";
const openai = new OpenAI();
class OpenAIMiddleware extends Middleware.BaseMiddleware {
id = "openai";
transformFunctionInput(args: Middleware.TransformFunctionInputArgs) {
return {
...args,
ctx: {
...args.ctx,
// Anything passed via `ctx` will be merged with the function's arguments
openai,
},
};
}
}Our Inngest Functions can now access the OpenAI client through the context:
ts
inngest.createFunction(
{ name: "user-create", triggers: { event: "app/user.create" } },
async ({ openai }) => {
const chatCompletion = await openai.chat.completions.create({
messages: [{ role: "user", content: "Say this is a test" }],
model: "gpt-3.5-turbo",
});
// ...
},
);Callout: 💡 Types are inferred from middleware outputs, so your Inngest functions will see an appropriately-typed openai property in their input.
Explore other examples in the TypeScript SDK Middleware examples page.
Advanced mutation
When transformFunctionInput() returns a new ctx, the types and data within that returned ctx are merged on top of the default provided by the library. This means that you can use a few tricks to overwrite data and types safely and more accurately.
For example, here we use a const assertion to infer the literal value of our foo example above.
ts
// In middleware
transformFunctionInput(args: Middleware.TransformFunctionInputArgs) {
return {
...args,
ctx: {
...args.ctx,
foo: "bar",
} as const,
};
}
// In a function
async ({ event, foo }) => {
// ^? (parameter) foo: "bar"
}Because the returned ctx object and the default are merged together, sometimes good inferred types are overwritten by more generic types from middleware. A common example of this might be when handling event data in middleware.
To get around this, you can provide the data but omit the type by using an as type assertion. For example, here we use a type assertion to add foo and alter the event data without affecting the type.
ts
transformFunctionInput(args: Middleware.TransformFunctionInputArgs) {
const event = await decrypt(args.ctx.event);
const newCtx = {
...args.ctx,
foo: "bar",
event,
};
return {
...args,
// Don't affect the `event` type
ctx: newCtx as Omit<typeof newCtx, "event">,
};
}Ordering middleware and types
Middleware runs in the order specified when registering it (see Middleware lifecycle), which affects typing too.
When inferring a mutated input or output, the SDK will apply changes from each middleware in sequence, just as it will at runtime. This means that for two middlewares that add a foo value to input arguments, the last one to run will be what it seen both in types and at runtime.
Our OpenAIMiddleware uses the transform_input hook to inject context:
py
import inngest
from openai import OpenAI
class OpenAIMiddleware(inngest.Middleware):
def __init__(
self,
client: inngest.Inngest,
raw_request: object,
) -> None:
self.openai = OpenAI(
# This is the default and can be omitted
api_key=os.environ.get("OPENAI_API_KEY"),
)
def transform_input(
self,
ctx: execution_lib.Context,
function: function.Function,
steps: step_lib.StepMemos,
) -> None:
ctx.openai = self.openai # type: ignore
inngest_client = inngest.Inngest(
app_id="my_app",
middleware=[OpenAIMiddleware],
)Our Inngest Functions can now access the openai client through the context:
py
@inngest_client.create_function(
fn_id="user-create",
trigger=inngest.TriggerEvent(event="app/user.create"),
)
async def fn(ctx: inngest.Context):
chat_completion = ctx.openai.chat.completions.create(
messages=[
{
"role": "user",
"content": "Say this is a test",
}
],
model="gpt-3.5-turbo",
)Callout: Support for Middleware in the Go SDK in planned.