Skip to main content
Version: Next (2.x)

Middlewares

Baeta provides a middleware system that lets you run logic before and after resolvers. Middlewares use a builder pattern — you chain $use calls onto a field, follow with .resolve(), and then pass the result into $fields to register it on the type.

Basic Usage

import { UserModule } from "./typedef.ts";

const { Query } = UserModule;

const userQuery = Query.user
.$use(async (next, { args }) => {
console.log("Before resolver:", args);
const result = await next();
console.log("After resolver:", result);
return result;
})
.resolve(({ args }) => {
return findUser(args.where);
});

export default Query.$fields({
user: userQuery,
});

Common Use Cases

Logging

const userQuery = Query.user
.$use(async (next) => {
const startTime = Date.now();
const result = await next();
console.log(`Query.user took ${Date.now() - startTime}ms`);
return result;
})
.resolve(({ args }) => {
return findUser(args.where);
});

Authentication

const userQuery = Query.user
.$use(async (next, { ctx }) => {
if (!ctx.user) {
throw new UnauthenticatedError();
}
return next();
})
.resolve(({ args }) => {
return findUser(args.where);
});

Data Transformation

const userQuery = Query.user
.$use(async (next) => {
const user = await next();
if (user) {
delete user.internalNotes;
}
return user;
})
.resolve(({ args }) => {
return findUser(args.where);
});

Error Handling

const userQuery = Query.user
.$use(async (next) => {
try {
return await next();
} catch (error) {
console.error("Error in user resolver:", error);
throw new GraphQLError("Failed to fetch user");
}
})
.resolve(({ args }) => {
return findUser(args.where);
});

Middleware Context

The second parameter provides the same context as resolvers:

  • args: Arguments passed to the resolver
  • ctx: Context object
  • source: Parent object
  • info: GraphQL resolve info

Multiple Middlewares

Middlewares are chained in order and execute outside-in:

const { Query } = UserModule;

const userQuery = Query.user
.$use(async (next) => {
console.log("First middleware - before");
const result = await next();
console.log("First middleware - after");
return result;
})
.$use(async (next) => {
console.log("Second middleware - before");
const result = await next();
console.log("Second middleware - after");
return result;
})
.resolve(({ args }) => {
return findUser(args.where);
});

// Output order:
// First middleware - before
// Second middleware - before
// Second middleware - after
// First middleware - after

Type-Level Middlewares

You can apply a middleware to all fields of a type by chaining $use on the type itself before $fields. This is useful for cross-cutting concerns like logging or authorization that apply to every field:

const { User } = UserModule;

const userResolver = User
.$use(async (next, { source }) => {
console.log("Resolving User field for:", source.id);
return next();
})
.$fields({
id: User.id.key("id"),
email: User.email.key("email"),
name: User.name.resolve(({ source }) => `${source.firstName} ${source.lastName}`),
});

Module-Level Middlewares

You can also apply a middleware to all fields across all types in a module by chaining $use on the module before $schema:

import { UserModule } from "./typedef.ts";

export default UserModule
.$use(async (next, { ctx }) => {
console.log("Request from:", ctx.userId);
return next();
})
.$schema({
User: userResolver,
Query: queryResolver,
});
tip

Middleware execution order is: module-level → type-level → field-level. This lets you set broad policies at the module level and refine them per type or field.