Skip to main content

Baeta

A schema-first, type-safe GraphQL framework designed for building clear, modular APIs at any scale.

Schema First

Use GraphQL SDL to design modular, easy-to-maintain schemas.

Modular By Design

Split your code into small, manageable modules for better maintainability.

Type Safe

Out-of-the-box type safety with automatic code generation.

Flexible & Extensible

Use only what you need — extend with plugins anytime.

Define the Schema

Leverage GraphQL SDL to define schemas for each module, keeping your API organized and easy to maintain.

modules/user/user.gql
type User {
id: ID!
name: String!
email: String!
age: Int
}

input UserWhereUnique {
id: ID
email: String
}

type Query {
user(where: UserWhereUnique!): User!
users: [User!]!
}

Implement the Resolvers

Baeta generates and enforces types automatically, so you can focus on writing simple, reliable resolvers.

modules/user/resolvers.ts
import { UserModule } from "./typedef.ts";

const { Query } = UserModule;

const userQuery = Query.user.resolve(({ args }) => {
return dataSource.user.find(args.where);
});

const usersQuery = Query.users.resolve(() => {
return dataSource.user.findMany();
});

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

Compose and Extend

Create modular schemas that are easy to grow and maintain. Extend types naturally as your API expands.

modules/user-photos/user-photos.gql
type Photo {
id: ID!
url: String!
description: String!
postedBy: User!
}

input PhotoCreateData {
url: String! @trim
description: String!
userId: ID!
}

extend type User {
photos: [Photo!]!
}

Scope-Based Authorization

Secure your API with granular, scope-based authorization. Baeta makes permission handling simple and consistent.

modules/user/user.queries.ts
import { auth, rule, scope } from "./lib/auth.ts";
import { UserModule } from "./typedef.ts";

const { Query } = UserModule;

const userQuery = Query.user
.$use(auth(rule.or(scope.isPublic, scope.isLoggedIn)))
.resolve(async ({ args }) => {
// ...
});

Simple, Effective Caching

Add automatic caching to your queries with minimal setup. Update cached data easily and predictably when mutations occur.

modules/user/user.cache.ts
import { createCache, defineQuery } from "@baeta/cache";
import { redisClient } from "./lib/redis.ts";
import { UserModule } from "./typedef.ts";

const { Query, Mutation } = UserModule;

export const userCache = createCache(redisClient, {
name: "UserCache",
parse: JSON.parse,
serialize: JSON.stringify,
})
.withQueries({
findUser: defineQuery({
resolve: async (args: { id: string }) => {
return dataSource.user.findUnique({ where: args });
},
}),
})
.build();

const userQuery = Query.user.map(({ args }) =>
userCache.queries.findUser({ id: args.where.id }),
);

const updateUserMutation = Mutation.updateUser
.$use(async (next) => {
const user = await next();
if (user) await userCache.update(user);
return user;
})
.resolve(async ({ args }) => {
// ...
});

Powerful custom directives

Add custom behavior exactly where you need it. Create your own directives for validation, transformation, or any custom logic.

lib/directives/trim.ts
import { createInputDirective } from "@baeta/core";

export const trimDirective = createInputDirective({
name: "trim",
target: "scalar",
resolve: ({ getValue, setValue }) => {
const value = getValue();
if (typeof value === "string") {
setValue(value.trim());
}
},
});