Functions

Comparisons

Compare Deesse Functions with other libraries

See how Deesse Functions compares to other popular API libraries and frameworks.

Feature Comparison Overview

Here's a quick comparison of key features across popular API libraries.

Overview

FeatureDeesse FunctionstRPCExpressFastifyZod APIs
Type Safety✅ Full✅ Full⚠️ Manual⚠️ Manual✅ Full
Context Management✅ Built-in❌ Limited❌ Manual❌ Manual❌ Manual
Input Validation✅ Zod✅ Zod❌ Manual❌ Manual✅ Zod
Caching✅ Built-in❌ Plugin❌ Manual❌ Plugin❌ Manual
Authorization✅ Built-in❌ Manual❌ Manual❌ Plugin❌ Manual
Result Types✅ Built-in❌ Throws❌ Throws❌ Throws❌ Throws
Event System✅ Built-in❌ Manual❌ Manual❌ Plugin❌ Manual
Learning CurveLowMediumLowLowMedium
Bundle SizeSmallMediumN/AN/ASmall

vs tRPC

Both libraries provide end-to-end type safety and use Zod for validation.

Key Differences

Context Management

tRPC: Context must be created and passed for each request Deesse Functions: Define once, use everywhere

// tRPC - Context must be created and passed for each request
const createContext = async (opts) => {
  return {
    userId: opts.req.session?.userId,
    prisma: new PrismaClient(),
  };
};

type Context = Awaited<ReturnType<typeof createContext>>;

// Deesse Functions - Define once, use everywhere
const { t, createAPI } = defineContext({
  userId: "guest",
  database: myDatabase,
});

Error Handling

// tRPC - Throws errors that must be caught
export const appRouter = router({
  getUser: procedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
    const user = await db.find(input.id);
    if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
    return user;
  }),
});

// Deesse Functions - Result types for explicit error handling
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);
  },
});

Caching

Built-in vs Plugin

tRPC requires plugins for caching. Deesse Functions has caching built-in.

// tRPC - Requires plugin and setup
import { initTRPC } from '@trpc/server';
import { Cache } from '@trpc/cache';

const t = initTRPC.context<Context>().create();
const cachedProcedure = t.procedure.use(Cache());

// Deesse Functions - Built-in, no plugin needed
const getUser = t.query({
  cacheKey: ['users', '{id}'],
  staleTime: 60000,
  handler: async (ctx, args) => {
    return success(await ctx.db.find(args.id));
  },
});

Authorization

// tRPC - Middleware must be manually created and applied
const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.userId) throw new TRPCError({ code: 'UNAUTHORIZED' });
  return next();
});

const protectedProcedure = t.procedure.use(isAuthed);

// Deesse Functions - Built-in authorization helpers
const deleteUser = t.mutation({
  authorize: hasPermission('admin'),
  handler: async (ctx, args) => {
    return success(await ctx.db.delete(args.id));
  },
});

Choose tRPC if:

You're already using Next.js App Router with tRPC's specific integrations

Choose tRPC if:

You need client-side type inference without code generation

Choose tRPC if:

You prefer middleware pattern over context injection

Choose Deesse Functions if:

You want better context management with automatic injection

Choose Deesse Functions if:

You need built-in caching without plugins

Choose Deesse Functions if:

You prefer Result types over thrown errors

Choose Deesse Functions if:

You need built-in authorization checks

Choose Deesse Functions if:

You want a simpler API without complex type-level programming

vs Express

Both are used for building APIs with TypeScript.

Key Differences

Type Safety

// Express - No built-in type safety
app.get('/users/:id', async (req, res) => {
  const id = req.params.id; // Type: any (or string with casting)
  const user = await db.find(id);
  res.json(user);
});

// Deesse Functions - Full type safety
const getUser = t.query({
  args: z.object({ id: z.number() }),
  handler: async (ctx, args) => {
    // args.id is type: number
    return success(await ctx.db.find(args.id));
  },
});

Error Handling

// Express - Manual error handling
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await db.find(req.params.id);
    if (!user) return res.status(404).json({ error: 'Not found' });
    res.json(user);
  } catch (error) {
    next(error);
  }
});

// Deesse Functions - Result types handle errors explicitly
const getUser = t.query({
  handler: async (ctx, args) => {
    const user = await ctx.db.find(args.id);
    if (!user) return failure(new Error("User not found"));
    return success(user);
  },
});

Context/Dependencies

Express: Manual dependency injection per route Deesse Functions: Context automatically available

// Express - Manual dependency injection per route
app.get('/users/:id', async (req, res) => {
  const db = req.app.get('database');
  const userId = req.session?.userId;
  const logger = req.app.get('logger');
  // Use them manually
});

// Deesse Functions - Context automatically available
const getUser = t.query({
  handler: async (ctx, args) => {
    // ctx.database, ctx.userId, ctx.logger available automatically
    return success(await ctx.database.find(args.id));
  },
});

Validation

// Express - Manual validation
app.post('/users', async (req, res) => {
  const schema = z.object({
    name: z.string().min(2),
    email: z.string().email(),
  });
  const result = schema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.errors });
  }
  // Use result.data
});

// Deesse Functions - Automatic validation
const createUser = t.mutation({
  args: z.object({
    name: z.string().min(2),
    email: z.string().email(),
  }),
  handler: async (ctx, args) => {
    // args is guaranteed to be valid
    return success(await ctx.database.create(args));
  },
});

Choose Express if:

You need maximum flexibility and minimal abstraction

Choose Express if:

You're migrating an existing Express app incrementally

Choose Express if:

You prefer traditional MVC patterns

Choose Express if:

You need very specific HTTP-level control

Choose Deesse Functions if:

You want end-to-end type safety

Choose Deesse Functions if:

You need better dependency injection

Choose Deesse Functions if:

You want automatic input validation

Choose Deesse Functions if:

You need built-in caching

Choose Deesse Functions if:

You prefer Result types over thrown errors

vs Fastify

Both can be used for building type-safe APIs with good performance.

Key Differences

Type Safety

// Fastify - Type safety requires schemas
fastify.get('/users/:id', {
  schema: {
    params: z.object({ id: z.number() }),
    response: { 200: UserSchema }
  }
}, async (request, reply) => {
  const user = await db.find(request.params.id);
  return user;
});

// Deesse Functions - Type safety is automatic
const getUser = t.query({
  args: z.object({ id: z.number() }),
  handler: async (ctx, args) => {
    return success(await ctx.database.find(args.id));
  },
});

Context/Dependencies

// Fastify - Use fastify.addHook to decorate request
fastify.addHook('onRequest', async (request, reply) => {
  request.userId = request.session?.userId;
});

fastify.get('/profile', async (request, reply) => {
  const user = await db.find(request.userId);
  return user;
});

// Deesse Functions - Context defined once
const { t, createAPI } = defineContext({
  userId: "guest",
  database: myDatabase,
});

const getProfile = t.query({
  handler: async (ctx, args) => {
    return success(await ctx.database.find(ctx.userId));
  },
});

Caching

Fastify: Requires plugin registration Deesse Functions: Built-in caching

// Fastify - Requires plugin registration
import fastifyCaching from '@fastify/caching';

fastify.register(fastifyCaching, {
  privacy: 'private',
  expiresIn: 300,
});

// Deesse Functions - Built-in caching
const getUser = t.query({
  cacheKey: ['users', '{id}'],
  staleTime: 300000,
  handler: async (ctx, args) => {
    return success(await ctx.db.find(args.id));
  },
});

Choose Fastify if:

You need the absolute best performance

Choose Fastify if:

You're building a traditional HTTP API

Choose Fastify if:

You want a plugin ecosystem for HTTP-specific features

Choose Fastify if:

You prefer decorator-style dependency injection

Choose Deesse Functions if:

Type safety is your top priority

Choose Deesse Functions if:

You need better context management

Choose Deesse Functions if:

You want built-in business logic features (caching, auth, events)

Choose Deesse Functions if:

You're not tied to HTTP as a transport

vs Zod APIs

Both use Zod for validation and provide type safety.

Key Differences

Scope

Zod alone: Only validation Deesse Functions: Complete API framework

// Zod alone - Only validation
const createUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

// You must manually:
// 1. Parse input
// 2. Handle validation errors
// 3. Call business logic
// 4. Handle errors
// 5. Return response

// Deesse Functions - Complete API framework
const createUser = t.mutation({
  args: z.object({
    name: z.string().min(2),
    email: z.string().email(),
  }),
  handler: async (ctx, args) => {
    // args is validated and typed
    // Business logic here
    // Result type handles errors
    return success(await ctx.database.create(args));
  },
});

Error Handling

// Zod alone - Must use try/catch
async function createUser(input: unknown) {
  const result = createUserSchema.safeParse(input);
  if (!result.success) {
    return { error: result.error.errors };
  }

  try {
    const user = await db.create(result.data);
    return { user };
  } catch (error) {
    return { error: error.message };
  }
}

// Deesse Functions - Result types built-in
const createUser = t.mutation({
  handler: async (ctx, args) => {
    try {
      return success(await ctx.database.create(args));
    } catch (error) {
      return failure(error);
    }
  },
});

Feature Comparison

Zod alone gives you:

  • ✅ Schema validation
  • ✅ Type inference
  • ❌ No context management
  • ❌ No caching
  • ❌ No authorization
  • ❌ No Result types
  • ❌ No events

Deesse Functions gives you:

  • ✅ Schema validation (via Zod)
  • ✅ Type inference
  • ✅ Context management
  • ✅ Built-in caching
  • ✅ Authorization checks
  • ✅ Result types
  • ✅ Event system

Choose Zod alone if:

You only need input validation

Choose Zod alone if:

You have a simple use case

Choose Zod alone if:

You want to build your own framework

Choose Deesse Functions if:

You need a complete API framework

Choose Deesse Functions if:

You want built-in business logic features

Choose Deesse Functions if:

You need better error handling patterns

Choose Deesse Functions if:

You want context management

vs Server Actions (Next.js)

Both provide type-safe server-side functions.

Key Differences

Context Management

// Server Actions - Must pass context manually
'use server';

async function createUser(formData: FormData) {
  const session = await getServerSession();
  const db = getPrismaClient();
  // Use them manually
}

// Deesse Functions - Context available automatically
const createUser = t.mutation({
  handler: async (ctx, args) => {
    // ctx.userId, ctx.database available
    return success(await ctx.database.create(args));
  },
});

Error Handling

// Server Actions - Throws errors by default
'use server';

async function deleteUser(id: number) {
  const user = await db.find(id);
  if (!user) throw new Error('User not found');
  await db.delete(id);
}

// Deesse Functions - Result types
const deleteUser = t.mutation({
  handler: async (ctx, args) => {
    const user = await ctx.database.find(args.id);
    if (!user) return failure(new Error("User not found"));
    await ctx.database.delete(args.id);
    return success({ deleted: true });
  },
});

Validation

// Server Actions - Manual validation
'use server';

import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

async function createUser(data: unknown) {
  const result = schema.safeParse(data);
  if (!result.success) {
    return { error: result.error.errors };
  }
  // Use result.data
}

// Deesse Functions - Automatic validation
const createUser = t.mutation({
  args: z.object({
    name: z.string().min(2),
    email: z.string().email(),
  }),
  handler: async (ctx, args) => {
    // args is guaranteed valid
    return success(await ctx.database.create(args));
  },
});

Choose Server Actions if:

You're fully committed to Next.js App Router

Choose Server Actions if:

You need simple form submissions

Choose Server Actions if:

You want React-specific features like optimistic updates

Choose Deesse Functions if:

You need better error handling patterns

Choose Deesse Functions if:

You want framework-agnostic code

Choose Deesse Functions if:

You need advanced features like caching and events

Choose Deesse Functions if:

You want reusable business logic not tied to React

Summary

When to Use Deesse Functions

Choose Deesse Functions if you need:

  1. Better Type Safety - Full end-to-end type safety without complex setup
  2. Context Management - Automatic dependency injection without manual passing
  3. Built-in Features - Caching, authorization, events, retries without plugins
  4. Explicit Error Handling - Result types instead of thrown errors
  5. Framework Agnostic - Use with any HTTP server or transport layer

When to Choose Alternatives

Choose alternatives if:

  1. tRPC - You need Next.js App Router-specific features
  2. Express - You need maximum flexibility with minimal abstraction
  3. Fastify - Performance is your only concern
  4. Zod alone - You only need validation, not a full framework
  5. Server Actions - You're fully committed to Next.js and React

Migration Guides

Quick Migration Examples

From Express to Deesse Functions

// Before (Express)
app.get('/users/:id', async (req, res) => {
  const user = await db.find(Number(req.params.id));
  if (!user) return res.status(404).json({ error: 'Not found' });
  res.json(user);
});

// After (Deesse 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);
  },
});

From tRPC to Deesse Functions

// Before (tRPC)
export const appRouter = router({
  getUser: procedure
    .input(z.object({ id: z.number() }))
    .query(async ({ input }) => {
      const user = await db.find(input.id);
      if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
      return user;
    }),
});

// After (Deesse 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);
  },
});

Next Steps

On this page