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
| Feature | Deesse Functions | tRPC | Express | Fastify | Zod 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 Curve | Low | Medium | Low | Low | Medium |
| Bundle Size | Small | Medium | N/A | N/A | Small |
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:
- Better Type Safety - Full end-to-end type safety without complex setup
- Context Management - Automatic dependency injection without manual passing
- Built-in Features - Caching, authorization, events, retries without plugins
- Explicit Error Handling - Result types instead of thrown errors
- Framework Agnostic - Use with any HTTP server or transport layer
When to Choose Alternatives
Choose alternatives if:
- tRPC - You need Next.js App Router-specific features
- Express - You need maximum flexibility with minimal abstraction
- Fastify - Performance is your only concern
- Zod alone - You only need validation, not a full framework
- 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);
},
});