← Back to DevBytes

Express vs Fastify: A Comprehensive Comparison for 2026

Introduction: The Node.js Framework Landscape in 2026

The Node.js ecosystem continues to evolve rapidly, and as we move into 2026, two server-side frameworks dominate the conversation: Express and Fastify. Express has been the de facto standard for over a decade, while Fastify has emerged as a high-performance challenger that addresses many of the pain points developers experience with traditional Express applications. This comprehensive guide will walk you through everything you need to know to make an informed decision between these two frameworks for your next project.

What Is Express?

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Express.js is a minimalist, unopinionated web application framework for Node.js. Created by TJ Holowaychuk in 2010, it became the most widely adopted Node.js framework and the foundation for countless other frameworks and libraries. Express provides a thin layer on top of Node's built-in HTTP module, offering routing, middleware support, and a simple API for building web applications and APIs.

Core Philosophy

Express follows a "minimalist" philosophy. It gives you just enough to build a web server without imposing strict patterns or conventions. This flexibility is both its greatest strength and its most significant weakness — you can structure your application however you like, but you're also responsible for making good architectural decisions.

What Is Fastify?

Fastify is a relatively newer web framework for Node.js, created by Matteo Collina and Tomas Della Vedova in 2016. It was designed from the ground up with performance as a primary concern, while also providing a robust plugin system, built-in schema-based validation, and excellent developer experience. Fastify is often described as "the fastest Node.js web framework" and benchmarks consistently support this claim.

Core Philosophy

Fastify is opinionated but extensible. It provides strong conventions around plugins, validation, serialization, and error handling while allowing you to extend functionality through its powerful plugin architecture. The framework prioritizes developer experience alongside raw performance, offering excellent TypeScript support, comprehensive logging, and automatic API documentation generation.

Why This Comparison Matters in 2026

As we enter 2026, several trends make this comparison more relevant than ever:

Installation and Basic Setup

Let's start with practical examples. Both frameworks are available via npm and require minimal setup to get a server running.

Express Setup

# Initialize a new project
npm init -y

# Install Express
npm install express

# For TypeScript support, install type definitions
npm install --save-dev @types/express typescript ts-node

Here's a minimal Express server in JavaScript:

// server.js
const express = require('express');
const app = express();
const PORT = 3000;

// Built-in middleware for parsing JSON bodies
app.use(express.json());

// A simple route
app.get('/hello', (req, res) => {
  res.json({ message: 'Hello from Express!' });
});

// Start the server
app.listen(PORT, () => {
  console.log(`Express server running on http://localhost:${PORT}`);
});

And the same server in TypeScript:

// server.ts
import express, { Request, Response } from 'express';

const app = express();
const PORT = 3000;

app.use(express.json());

app.get('/hello', (req: Request, res: Response) => {
  res.json({ message: 'Hello from Express with TypeScript!' });
});

app.listen(PORT, () => {
  console.log(`Express server running on http://localhost:${PORT}`);
});

Fastify Setup

# Initialize a new project
npm init -y

# Install Fastify (TypeScript types are included)
npm install fastify

A minimal Fastify server in JavaScript:

// server.js
const fastify = require('fastify');

// Create a Fastify instance with logging enabled
const app = fastify({ logger: true });

// A simple route with schema validation
app.get('/hello', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          message: { type: 'string' }
        }
      }
    }
  },
  handler: async (request, reply) => {
    return { message: 'Hello from Fastify!' };
  }
});

// Start the server
const start = async () => {
  try {
    await app.listen({ port: 3000 });
    console.log('Fastify server running on http://localhost:3000');
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
};

start();

Fastify with TypeScript (types are built-in, no additional packages needed):

// server.ts
import fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

const app: FastifyInstance = fastify({ logger: true });

app.get('/hello', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          message: { type: 'string' }
        }
      }
    }
  },
  handler: async (request: FastifyRequest, reply: FastifyReply) => {
    return { message: 'Hello from Fastify with TypeScript!' };
  }
});

const start = async () => {
  try {
    await app.listen({ port: 3000 });
    console.log('Fastify server running on http://localhost:3000');
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
};

start();

Routing Comparison

Routing is the backbone of any web framework. Both Express and Fastify offer routing capabilities, but their approaches differ significantly.

Express Routing

Express uses a middleware-based routing system. Routes are matched in the order they are defined, and you can attach multiple handlers to a single route. Express supports both inline route definitions and the Router module for organizing routes.

// express-routing.js
const express = require('express');
const app = express();

// Basic routing with path parameters
app.get('/users/:userId', (req, res) => {
  const { userId } = req.params;
  res.json({ userId, name: 'John Doe' });
});

// Multiple handlers on one route (middleware chaining)
app.get('/admin/dashboard',
  (req, res, next) => {
    console.log('Auth check middleware');
    // Simulate authentication
    req.user = { role: 'admin' };
    next();
  },
  (req, res) => {
    res.json({ dashboard: 'Admin data', user: req.user });
  }
);

// Using Router for modular route organization
const userRouter = express.Router();

userRouter.get('/profile', (req, res) => {
  res.json({ profile: 'User profile data' });
});

userRouter.post('/settings', (req, res) => {
  const { theme } = req.body;
  res.json({ message: `Settings updated, theme: ${theme}` });
});

// Mount the router at a path prefix
app.use('/api/users', userRouter);

// Wildcard and regex routes
app.get('/files/*', (req, res) => {
  res.json({ path: req.params[0] });
});

app.listen(3000);

Fastify Routing

Fastify's routing is built on find-my-way, a high-performance router based on a Radix tree data structure. This means route lookup is extremely fast and consistent regardless of the number of routes. Fastify also encourages declaring routes with JSON schemas for input validation and output serialization.

// fastify-routing.js
const fastify = require('fastify')({ logger: true });

// Basic routing with path parameters and schema validation
fastify.get('/users/:userId', {
  schema: {
    params: {
      type: 'object',
      required: ['userId'],
      properties: {
        userId: { type: 'string', pattern: '^[a-f0-9-]+$' }
      }
    },
    response: {
      200: {
        type: 'object',
        properties: {
          userId: { type: 'string' },
          name: { type: 'string' }
        }
      }
    }
  },
  handler: async (request, reply) => {
    const { userId } = request.params;
    return { userId, name: 'John Doe' };
  }
});

// Route-level hooks instead of inline middleware
fastify.get('/admin/dashboard', {
  preHandler: async (request, reply) => {
    // Simulate authentication
    request.user = { role: 'admin' };
  },
  handler: async (request, reply) => {
    return { dashboard: 'Admin data', user: request.user };
  }
});

// Using plugins for modular route organization
async function userRoutes(fastify, options) {
  fastify.get('/profile', async (request, reply) => {
    return { profile: 'User profile data' };
  });

  fastify.post('/settings', {
    schema: {
      body: {
        type: 'object',
        properties: {
          theme: { type: 'string' }
        }
      }
    },
    handler: async (request, reply) => {
      const { theme } = request.body;
      return { message: `Settings updated, theme: ${theme}` };
    }
  });
}

// Register routes as a plugin with a prefix
fastify.register(userRoutes, { prefix: '/api/users' });

// Wildcard routes are also supported
fastify.get('/files/*', async (request, reply) => {
  return { path: request.params['*'] };
});

const start = async () => {
  await fastify.listen({ port: 3000 });
};

start();

Middleware vs Plugins and Hooks

This is one of the most fundamental architectural differences between the two frameworks.

Express Middleware

Express uses a sequential middleware pipeline. Each middleware function receives (req, res, next) and can modify the request/response objects, end the response, or pass control to the next middleware by calling next(). This model is simple but can lead to callback-based complexity and makes it harder to handle asynchronous operations cleanly.

// express-middleware.js
const express = require('express');
const app = express();

// Custom middleware for request logging
app.use((req, res, next) => {
  const start = Date.now();
  console.log(`${req.method} ${req.url} - started`);

  // Override res.end to log completion
  const originalEnd = res.end;
  res.end = function (...args) {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} - completed in ${duration}ms`);
    return originalEnd.apply(this, args);
  };

  next();
});

// Error-handling middleware (must have 4 parameters)
app.use((err, req, res, next) => {
  console.error('Error:', err.message);
  res.status(500).json({ error: 'Internal Server Error' });
});

// Route that may throw an error
app.get('/risky', (req, res, next) => {
  try {
    // Some operation that might fail
    throw new Error('Something went wrong');
  } catch (err) {
    next(err); // Pass error to error-handling middleware
  }
});

app.listen(3000);

Fastify Plugins and Hooks

Fastify replaces the middleware concept with a more structured system of plugins and lifecycle hooks. Plugins are encapsulated, scoped, and can be loaded asynchronously. Hooks like onRequest, preHandler, onSend, and onError provide precise insertion points in the request lifecycle. This model promotes better separation of concerns and eliminates the need for manual next() calls.

// fastify-plugins-hooks.js
const fastify = require('fastify')({ logger: true });

// Global hook: runs before each request
fastify.addHook('onRequest', async (request, reply) => {
  request.startTime = Date.now();
  fastify.log.info(`${request.method} ${request.url} - started`);
});

// Global hook: runs when response is being sent
fastify.addHook('onSend', async (request, reply, payload) => {
  const duration = Date.now() - request.startTime;
  fastify.log.info(`${request.method} ${request.url} - completed in ${duration}ms`);
  // You can modify the payload here
  return payload;
});

// Global error handler
fastify.setErrorHandler((error, request, reply) => {
  fastify.log.error(`Error: ${error.message}`);
  reply.status(500).send({ error: 'Internal Server Error' });
});

// Plugin: encapsulated functionality with its own hooks and routes
async function featurePlugin(fastify, options) {
  // Hook scoped to this plugin only
  fastify.addHook('preHandler', async (request, reply) => {
    request.pluginData = { feature: options.name };
  });

  fastify.get('/feature-data', async (request, reply) => {
    return { data: request.pluginData };
  });
}

// Register plugin with options
fastify.register(featurePlugin, { name: 'myFeature' });

// Route-level hooks
fastify.get('/risky', {
  preHandler: async (request, reply) => {
    // Validation or setup logic
  },
  handler: async (request, reply) => {
    // If something goes wrong, just throw
    throw new Error('Something went wrong');
    // Fastify catches this and passes to error handler automatically
  }
});

const start = async () => {
  await fastify.listen({ port: 3000 });
};

start();

Request Validation and Serialization

One area where Fastify dramatically outshines Express is built-in schema-based validation and serialization.

Express: Manual Validation

Express leaves validation entirely up to you. You typically need third-party libraries like Joi, Zod, or express-validator, and you must manually integrate them into your route handlers.

// express-validation.js
const express = require('express');
const app = express();
app.use(express.json());

// Manual validation with a library like Zod (popular in 2026)
const { z } = require('zod');

const userSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().min(0).max(150).optional()
});

app.post('/users', (req, res, next) => {
  try {
    const validatedData = userSchema.parse(req.body);
    // Data is validated, proceed
    res.status(201).json({
      message: 'User created',
      user: validatedData
    });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({
        error: 'Validation failed',
        details: error.errors.map(e => ({
          field: e.path.join('.'),
          message: e.message
        }))
      });
    }
    next(error);
  }
});

app.listen(3000);

Fastify: Built-in Schema Validation

Fastify uses JSON Schema (via Ajv) for validation. You define schemas directly in your route options, and Fastify automatically validates request inputs and serializes response outputs. This is not only faster (Ajv is highly optimized) but also reduces boilerplate significantly.

// fastify-validation.js
const fastify = require('fastify')({ logger: true });

// Define a reusable schema
const userBodySchema = {
  type: 'object',
  required: ['name', 'email'],
  properties: {
    name: { type: 'string', minLength: 2, maxLength: 50 },
    email: { type: 'string', format: 'email' },
    age: { type: 'integer', minimum: 0, maximum: 150 }
  },
  additionalProperties: false
};

const userResponseSchema = {
  type: 'object',
  properties: {
    message: { type: 'string' },
    user: {
      type: 'object',
      properties: {
        id: { type: 'string' },
        name: { type: 'string' },
        email: { type: 'string' },
        age: { type: 'integer' }
      }
    }
  }
};

fastify.post('/users', {
  schema: {
    body: userBodySchema,
    response: {
      201: userResponseSchema,
      400: {
        type: 'object',
        properties: {
          error: { type: 'string' },
          details: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                field: { type: 'string' },
                message: { type: 'string' }
              }
            }
          }
        }
      }
    }
  },
  handler: async (request, reply) => {
    // request.body is already validated — no manual parsing needed
    const { name, email, age } = request.body;
    const user = { id: 'usr_123', name, email, age: age || null };
    
    reply.status(201);
    return { message: 'User created', user };
    // Response is automatically serialized according to the schema
  }
});

// Fastify also supports response serialization optimization
// It uses fast-json-stringify for schema-based serialization
// which is significantly faster than JSON.stringify

const start = async () => {
  await fastify.listen({ port: 3000 });
};

start();

Asynchronous Handling and Error Management

How each framework handles async operations and errors has a profound impact on code reliability.

Express Async Challenges

Express does not natively handle promise rejections. If an async route handler throws an error, Express won't catch it unless you explicitly wrap it or use a library. This has been a source of countless production bugs.

// express-async-issues.js
const express = require('express');
const app = express();

// DANGEROUS: Unhandled promise rejection will crash the process
app.get('/dangerous', async (req, res) => {
  // If this throws, Express won't catch it
  const data = await fetchDataThatMightFail();
  res.json(data);
});

// SAFE: Manual try/catch wrapping
app.get('/safe-manual', async (req, res, next) => {
  try {
    const data = await fetchDataThatMightFail();
    res.json(data);
  } catch (err) {
    next(err);
  }
});

// MODERN APPROACH (2026): Use a wrapper library like express-async-errors
// or a custom wrapper function
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.get('/safe-wrapper', asyncHandler(async (req, res) => {
  const data = await fetchDataThatMightFail();
  res.json(data);
}));

// Or use express-async-errors (popular in 2026)
require('express-async-errors');
// Now this works without try/catch:
app.get('/safe-library', async (req, res) => {
  const data = await fetchDataThatMightFail();
  res.json(data);
});

app.listen(3000);

async function fetchDataThatMightFail() {
  throw new Error('Database connection failed');
}

Fastify Native Async Support

Fastify was designed for async/await from day one. It natively handles promise rejections and automatically passes them to the error handler. This means you can write clean async handlers without worrying about unhandled rejections.

// fastify-async.js
const fastify = require('fastify')({ logger: true });

// Native async support — no wrapper needed
fastify.get('/data', async (request, reply) => {
  // Fastify catches promise rejections automatically
  const data = await fetchDataThatMightFail();
  return data;
  // You can also use reply.send() but returning is preferred
});

// Custom error handler receives all errors
fastify.setErrorHandler((error, request, reply) => {
  fastify.log.error(error);
  
  // Differentiate error types
  if (error.code === 'FETCH_ERROR') {
    reply.status(503).send({ error: 'External service unavailable' });
  } else {
    reply.status(500).send({ error: 'Internal server error' });
  }
});

// Async hooks are also fully supported
fastify.addHook('preHandler', async (request, reply) => {
  // Async operations in hooks work seamlessly
  const session = await loadSession(request);
  request.session = session;
});

const start = async () => {
  await fastify.listen({ port: 3000 });
};

start();

async function fetchDataThatMightFail() {
  const error = new Error('Database connection failed');
  error.code = 'FETCH_ERROR';
  throw error;
}

async function loadSession(request) {
  return { userId: '123', role: 'user' };
}

TypeScript Experience

In 2026, TypeScript support is a critical consideration. Here's how the two frameworks compare.

Express with TypeScript

Express requires separate @types/express packages. The type system works but can be cumbersome — the Request and Response types are broad, and you often need to extend them manually for custom properties. Route handlers don't infer types from validation schemas because Express doesn't have built-in schemas.

// express-typescript.ts
import express, { Request, Response, NextFunction } from 'express';

// Extending Request type for custom properties
interface CustomRequest extends Request {
  userId?: string;
  session?: {
    role: string;
    permissions: string[];
  };
}

const app = express();
app.use(express.json());

// Type-safe route with extended Request
app.get('/dashboard', async (req: CustomRequest, res: Response, next: NextFunction) => {
  try {
    // req.userId is typed but not guaranteed at runtime
    const userId = req.userId;
    // Manual runtime check still needed
    if (!userId) {
      return res.status(401).json({ error: 'Unauthorized' });
    }
    res.json({ dashboard: `Data for ${userId}` });
  } catch (err) {
    next(err);
  }
});

app.listen(3000);

Fastify with TypeScript

Fastify includes first-class TypeScript support with no additional packages. The framework provides generic type parameters for request and reply, and you can define typed schemas that infer both validation and handler types. In 2026, Fastify also supports type providers like TypeBox for even stronger typing.

// fastify-typescript.ts
import fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

// Using TypeBox for schema definition with automatic type inference
// TypeBox is a popular choice in 2026 for typed JSON Schema
import { Type, Static } from '@sinclair/typebox';

const app: FastifyInstance = fastify({ logger: true });

// Define schema with TypeBox — types are inferred automatically
const UserBody = Type.Object({
  name: Type.String({ minLength: 2, maxLength: 50 }),
  email: Type.String({ format: 'email' }),
  age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 }))
});

type UserBodyType = Static;

const UserResponse = Type.Object({
  id: Type.String(),
  name: Type.String(),
  email: Type.String(),
  age: Type.Optional(Type.Integer())
});

// Route with full type inference
app.post('/users', {
  schema: {
    body: UserBody,
    response: {
      201: UserResponse
    }
  },
  handler: async (request: FastifyRequest<{ Body: UserBodyType }>, reply: FastifyReply) => {
    // request.body is fully typed — no manual validation needed
    const { name, email, age } = request.body;
    
    // TypeScript knows the exact shape of request.body
    const user = {
      id: 'usr_123',
      name,  // TypeScript knows this is string
      email, // TypeScript knows this is string
      age    // TypeScript knows this is number | undefined
    };
    
    reply.status(201);
    return user; // Response matches UserResponse schema
  }
});

// Declaring custom properties on request using decorators
// Fastify provides a type-safe way to extend the request
declare module 'fastify' {
  interface FastifyRequest {
    userId?: string;
    session?: {
      role: string;
      permissions: string[];
    };
  }
}

app.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => {
  request.userId = '123';
  request.session = { role: 'admin', permissions: ['read', 'write'] };
});

const start = async (): Promise => {
  try {
    await app.listen({ port: 3000 });
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
};

start();

Performance Benchmarks

Performance is a key differentiator. Fastify consistently benchmarks as one of the fastest Node.js frameworks, while Express sits in the middle of the pack.

Understanding the Performance Gap

Fastify achieves its performance through several optimizations:

Here's a simple benchmark you can run to see the difference:

// benchmark.js — requires both express and fastify installed
const express = require('express');
const fastify = require('fastify');

// --- Express setup ---
const expressApp = express();
expressApp.get('/bench', (req, res) => {
  res.json({ message: 'Hello' });
});

// --- Fastify setup ---
const fastifyApp = fastify({ logger: false });
fastifyApp.get('/bench', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: { message: { type: 'string' } }
      }
    }
  },
  handler: async () => ({ message: 'Hello' })
});

// --- Benchmark runner ---
const autocannon = require('autocannon');

async function runBenchmark(name, url) {
  console.log(`\n=== Benchmarking ${name} ===`);
  const results = await autocannon({
    url,
    connections: 100,
    duration: 30,
    pipelining: 1
  });
  console.log(`${name} - Requests/sec: ${results.requests.average}`);
  console.log(`${name} - Latency (avg): ${results.latency.average}ms`);
  console.log(`${name} - Throughput: ${results.throughput.average} bytes/sec`);
}

async function main() {
  const EXPRESS_PORT = 3001;
  const FASTIFY_PORT = 3002;

  const expressServer = expressApp.listen(EXPRESS_PORT);
  const fastifyServer = await fastifyApp.listen({ port: FASTIFY_PORT });

  await runBenchmark('Express', `http://localhost:${EXPRESS_PORT}/bench`);
  await runBenchmark('Fastify', `http://localhost:${FASTIFY_PORT}/bench`);

  expressServer.close();
  await fastifyServer.close();
}

main().catch(console.error);

// Typical results in 2026 (Node.js 22+):
// Express - Requests/sec: ~45,000 - ~55,000
// Fastify - Requests/sec: ~75,000 - ~95,000
// Fastify is roughly 1.5x to 2x faster in throughput
// Latency difference is even more pronounced under load

Plugin Ecosystem and Extensibility

Express Middleware Ecosystem

Express benefits from a massive, mature ecosystem. There's a middleware package for virtually every need — from authentication to compression to rate limiting. However, quality varies widely, and many packages are unmaintained.

// express-ecosystem.js
const express = require('express');
const app = express();

// Compression middleware
const compression = require('compression');
app.use(compression());

// Helmet for security headers
const helmet = require('helmet');
app.use(helmet());

// Rate limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  standardHeaders: true,
  legacyHeaders: false
});
app.use('/api/', limiter);

// CORS
const cors = require('cors');
app.use(cors({ origin: 'https://myapp.com' }));

// Session management
const session = require('express-session');
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, httpOnly: true }
}));

app.get('/', (req, res) => {
  res.json({ message: 'Express with ecosystem middleware' });
});

app.listen(3000);

Fastify Plugin Ecosystem

Fastify's plugin system is more structured. Plugins are encapsulated, can have their own hooks and routes, and are loaded asynchronously with dependency management. The official plugin ecosystem is curated and well-maintained, though smaller than Express's.

// fastify-ecosystem.js
const fastify = require('fastify')({ logger: true });

async function setupPlugins() {
  // Compression — built-in via @fastify/compress
  await fastify.register(require('@fastify/compress'), {
    global: true,
    encodings: ['gzip', 'deflate']
  });

  // Security headers — @fastify/helmet
  await fastify.register(require('@fastify/helmet'), {
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"]
      }
    }
  });

  // Rate limiting — @fastify/rate-limit
  await fastify.register(require('@fastify/rate-limit'), {
    max: 100,
    timeWindow: '15 minutes',
    hook: 'onRequest'
  });

  // CORS — @fastify/cors
  await fastify.register(require('@fastify/cors'), {
    origin: 'https://myapp.com'
  });

  // Sessions — @fastify/session
  await fastify.register(require('@fastify/session'), {
    secret: 'a-very-long-and-secure-secret-key-at-least-32-characters',
    cookie: { secure: true, httpOnly: true }
  });
}

async function main() {
  await setupPlugins();

  fastify.get('/', async (request, reply) => {
    return { message: 'Fastify with ecosystem plugins' };
  });

  await fastify.listen({ port: 3000 });
}

main().catch(err => {
  fastify.log.error(err);
  process.exit(1);
});

Building a Real-World API: Side-by-Side Comparison

Let's build the same REST API for a task management system in both frameworks to see the practical differences.

Express Implementation

// express-task-api.js
const express = require('express');
const app = express();
const PORT = 3000;

// Middleware setup
app.use(express.json());
app.use(require('helmet')());
app.use(require('cors')());

// In-memory database simulation
const tasks = new Map();
let taskIdCounter = 1;

// Validation helper (using Zod in 2026)
const { z } = require('zod');

const createTaskSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().max(1000).optional(),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
  dueDate: z.string().datetime().optional()
});

const updateTaskSchema = z.object({
  title: z.string().min(1).max(200).optional(),
  description: z.string().max(1000).optional(),
  priority: z.enum(['low', 'medium', 'high']).optional(),
  status: z.enum(['pending', 'in-progress', 'completed']).optional(),
  dueDate: z.string().datetime().optional()
});

// Async handler wrapper
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Routes
app.get('/api/tasks', asyncHandler(async (req, res) => {
  const allTasks = Array.from(tasks.values());
  res.json({ tasks: allTasks, total: allTasks.length });
}));

app.get('/api/tasks/:id', asyncHandler(async (req, res) => {
  const task = tasks.get(req.params.id);
  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }
  res.json(task);
}));

app.post('/api/tasks', asyncHandler(async (req, res) => {
  const validated = createTaskSchema.parse(req.body);
  const id = `task_${taskIdCounter++}`;
  const task = {
    id,
    ...validated,
    status: 'pending',
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };
  tasks.set(id, task);
  res.status(201

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles