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 resolverctx: Context objectsource: Parent objectinfo: 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,
});
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.