Functions

Quick Start

Get started with DeesseJS Functions in minutes

Get up and running with DeesseJS Functions in under 5 minutes.

Installation

Install the package using your preferred package manager:

# npm
npm install @deessejs/functions

# pnpm
pnpm add @deessejs/functions

# yarn
yarn add @deessejs/functions

Peer Dependency: DeesseJS Functions requires Zod for schema validation.

npm install zod

Basic Usage

Let's create your first API with Deesse Functions.

1. Define Context

Context is your single source of truth - define it once, use it everywhere:

import { defineContext, success } from '@deessejs/functions';
import { z } from 'zod';

const { t, createAPI } = defineContext({
  userId: "guest",
  locale: "en",
});

This code does three things:

  • defineContext() creates your context with the initial values you provide
  • t is a builder object you'll use to define queries and mutations
  • createAPI() activates your endpoints and makes them available to call

The context values (userId, locale) will be automatically available in all your query and mutation handlers - no need to pass them manually.

2. Create Queries and Mutations

Queries are for reading data, Mutations are for writing data:

// A query to fetch a user
const getUser = t.query({
  args: z.object({ id: z.number() }),
  handler: async (ctx, args) => {
    // Access context with ctx.userId, ctx.locale, etc.
    // Return success with data
    return success({ id: args.id, name: "Alice" });
  },
});

// A mutation to create a user
const createUser = t.mutation({
  args: z.object({
    name: z.string().min(2),
    email: z.string().email(),
  }),
  handler: async (ctx, args) => {
    // Create user in your database
    const newUser = { id: 1, ...args };
    return success(newUser);
  },
});

Each endpoint has three parts:

  • t.query() or t.mutation() - Defines whether this reads or writes data
  • args - A Zod schema that validates input automatically before your handler runs
  • handler - Your function where ctx (context) and args (validated input) are available
  • success() - Returns a successful result containing your data

3. Create Your API

Combine your endpoints into an API:

const api = createAPI({
  users: t.router({
    get: getUser,
    create: createUser,
  }),
});

Routers organize related endpoints together. You can nest them deeply:

  • api.users.get() - Calls the getUser query
  • api.users.create() - Calls the createUser mutation

The t.router() function groups endpoints logically, making your API structure cleaner and easier to navigate.

4. Use Your API

Call your endpoints with full type safety:

// Fetch a user
const result = await api.users.get({ id: 1 });

if (result.success) {
  console.log(result.value); // { id: 1, name: "Alice" }
} else {
  console.error(result.error);
}

// Create a new user
const newUser = await api.users.create({
  name: "Bob",
  email: "bob@example.com",
});

This demonstrates type safety in action:

  • Arguments are validated by Zod - passing invalid types like { id: "wrong" } will fail at runtime
  • Return values are fully typed - TypeScript knows exactly what properties result.value contains
  • Errors are handled explicitly with the result.success check
  • No more any types or manual type casting needed!

Complete Example

Here's a complete working example:

import { defineContext, success } from '@deessejs/functions';
import { z } from 'zod';

// 1. Define context with your dependencies
const { t, createAPI } = defineContext({
  database: myDatabase,
  userId: "user-123",
});

// 2. Define queries (read operations)
const getUser = t.query({
  args: z.object({ id: z.number() }),
  handler: async (ctx, args) => {
    const user = await ctx.database.users.find(args.id);
    return success(user);
  },
});

const listPosts = t.query({
  args: z.object({
    limit: z.number().min(1).max(100).default(20),
  }),
  handler: async (ctx, args) => {
    const posts = await ctx.database.posts.findAll({ limit: args.limit });
    return success(posts);
  },
});

// 3. Define mutations (write operations)
const createPost = t.mutation({
  args: z.object({
    title: z.string().min(1).max(200),
    content: z.string().min(10),
  }),
  handler: async (ctx, args) => {
    const post = await ctx.database.posts.create({
      ...args,
      authorId: ctx.userId,
    });
    return success(post);
  },
});

// 4. Create and export the API
export const api = createAPI({
  users: t.router({
    get: getUser,
  }),
  posts: t.router({
    list: listPosts,
    create: createPost,
  }),
});

Next Steps

Type Safety in Action

Deesse Functions provides full end-to-end type safety:

// Arguments are type-checked
const user = await api.users.get({ id: 1 });
const user = await api.users.get({ id: "wrong" }); // ❌ Type error!

// Return types are inferred
user.value.name; // ✅ TypeScript knows this exists
user.value.wrongField; // ❌ Type error!

Error Handling

Handle errors gracefully with Result types:

import { success, failure } from '@deessejs/functions';

const getUser = t.query({
  args: z.object({ id: z.number() }),
  handler: async (ctx, args) => {
    const user = await ctx.database.find(args.id);

    if (!user) {
      return failure(new Error("User not found"));
    }

    return success(user);
  },
});

// Usage
const result = await api.users.get({ id: 999 });

if (!result.success) {
  console.error("Failed:", result.error.message);
  // Handle error
}

// Access value safely
if (result.success) {
  console.log(result.value); // User data
}

Validation is Automatic

All arguments are validated automatically using Zod schemas. Invalid inputs return validation errors without running your handler.

TypeScript Configuration

Ensure your tsconfig.json has the following settings for the best experience:

{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}

Need Help?

On this page