Skip to main content

Command Palette

Search for a command to run...

Why I Gave Up on Express.js for Our API and Moved to Fastify Instead

Published
6 min readView as Markdown

If you’ve spent hours chasing mysterious performance bugs in Express.js, you’re not alone. I’ve been there—writing APIs, hitting scaling ceilings, and wondering if there was a better way. It took me months (and honestly, some stubbornness) before I gave Fastify a real shot. But once I did, our API started breathing easier, and my weekend debugging sessions dropped dramatically.

Why Express.js Started to Hurt

I’ve built APIs with Express.js for years. It’s familiar, straightforward, and has a huge ecosystem. But as our user base grew, so did our pain points.

One of the biggest issues? Performance. Express.js wasn’t designed for high throughput. I noticed that as our endpoints got busier, latency crept up and memory usage ballooned. We’d hit bottlenecks, especially when handling lots of simultaneous requests.

The thing is, Express.js isn’t inherently slow—it’s just not optimized for speed. Middleware runs in series, request parsing isn’t the fastest, and there’s a lot of legacy code under the hood.

When our API started serving thousands of requests per minute, Express.js felt like a bottleneck. So, after one too many slowdowns, I decided it was time to try Fastify.

What Makes Fastify Different?

Fastify’s motto is “fast and low overhead.” And it delivers.

  • Schema-based validation: Built-in, not bolted on.
  • Plugin architecture: Encourages modularity.
  • Lifecycle hooks: More granular control.
  • Out-of-the-box performance: Designed for speed with async everywhere.

Most importantly, it feels familiar. If you’re used to Express.js, the switch isn’t painful.

Code Example: Hello World Comparison

Let me show you what I mean. Here’s a basic Express.js server:

// Express.js Hello World
const express = require('express');
const app = express();

app.get('/hello', (req, res) => {
  // Respond with JSON
  res.json({ message: 'Hello, Express!' });
});

app.listen(3000, () => {
  // Server is running
  console.log('Express server listening on port 3000');
});

Now, the same in Fastify:

// Fastify Hello World
const fastify = require('fastify')({ logger: true });

fastify.get('/hello', async (request, reply) => {
  // Respond with JSON
  return { message: 'Hello, Fastify!' };
});

fastify.listen({ port: 3000 }, (err, address) => {
  // Server is running
  if (err) throw err;
  fastify.log.info(`Fastify server listening on ${address}`);
});

Notice the similarities? But Fastify is already doing more under the hood. The built-in logger, async handler, and response serialization are faster and more reliable.

The Real Win: Schema Validation

One thing that always bugged me with Express.js was input validation. You’d reach for third-party libraries like express-validator or Joi, glue them in, and hope you didn’t miss something.

With Fastify, validation is first-class. You define schemas right in your route, and Fastify takes care of the rest.

Code Example: Schema Validation in Fastify

Here’s a practical example—a POST endpoint that expects a JSON body:

// Fastify with schema validation for a POST endpoint
const fastify = require('fastify')();

fastify.post('/user', {
  schema: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string' },
        email: { type: 'string', format: 'email' },
      }
    }
  }
}, async (request, reply) => {
  // Fastify validates request body before reaching this handler
  const { name, email } = request.body;
  // Save user logic goes here
  return { status: 'success', name, email };
});

fastify.listen({ port: 3000 });
  • No need for extra middleware: Validation happens before your handler.
  • Automatic error responses: Fastify returns 400 Bad Request if schema fails.

Express.js? You’d need to add middleware and write custom error handling.

Scaling: Where Fastify Shines

The moment I really felt the difference was when we pushed our API harder. With Express.js, adding new endpoints meant carefully managing middleware order and worrying about memory leaks.

Fastify’s plugin system keeps things isolated. You can register plugins for routes, authentication, or anything else, and they don’t interfere. That modularity saved our sanity.

Code Example: Fastify Plugin for Authentication

Here’s how we handled authentication as a plugin:

// Fastify authentication plugin
async function authPlugin(fastify, options) {
  fastify.decorate('authenticate', async function(request, reply) {
    // Simple token check
    const token = request.headers['authorization'];
    if (!token || token !== 'mysecrettoken') {
      // Unauthorized response
      reply.code(401).send({ error: 'Unauthorized' });
    }
  });

  // Register a protected route
  fastify.get('/protected', { preHandler: fastify.authenticate }, async (request, reply) => {
    // Only runs if authenticated
    return { secret: '42' };
  });
}

const fastify = require('fastify')();
fastify.register(authPlugin);

fastify.listen({ port: 3000 });
  • decorate: Adds reusable functions to Fastify instance.
  • preHandler: Runs authentication before route handler.
  • Plugin registration: Keeps authentication logic separate.

This pattern made it easy to add or remove features without breaking the rest of our API.

Migration Frustrations (and Solutions)

Switching frameworks isn’t painless, and I hit my share of snags. Here’s what tripped us up:

  • Middleware vs. hooks: Express.js middleware works differently than Fastify hooks. We had to rethink request lifecycle logic.
  • Error handling: Fastify’s error system is more opinionated. Custom errors require different handling.
  • Third-party support: Not every Express.js middleware has a Fastify equivalent. Sometimes you have to rewrite or find alternatives.

I spent a weekend debugging a CORS issue because I assumed the Express.js middleware would “just work” in Fastify. Turns out, Fastify has its own CORS plugin (@fastify/cors), and it’s better to use that.

Common Mistakes When Switching to Fastify

I’ve seen (and made) these mistakes more than once:

  1. Treating Fastify like Express.js: It’s tempting to write Fastify code as if you’re still in Express. But lifecycle hooks, error handling, and plugin registration are different. Read the docs—seriously.

  2. Ignoring schema validation: Fastify’s validation is powerful, but only if you use it. Skipping schemas means you miss out on automatic error handling and performance benefits.

  3. Porting Express middleware directly: Not every middleware works out-of-the-box with Fastify. Use Fastify plugins where possible, or rewrite middleware to fit Fastify’s async style.

Key Takeaways

  • Fastify offers a genuine performance boost for APIs under load, thanks to its async-first design and efficient request handling.
  • Built-in schema validation saves time, reduces bugs, and improves reliability compared to bolted-on Express middleware.
  • The plugin architecture encourages modular code, making scaling and refactoring less painful.
  • Migrating isn’t “plug-and-play”—expect to refactor middleware and rethink error handling.
  • Fastify feels familiar but rewards you for learning its patterns and using its features fully.

Closing Thoughts

Switching from Express.js to Fastify wasn’t just about chasing speed—it was about making our API easier to maintain and scale. I still appreciate Express for small projects and prototyping, but for anything serious, Fastify is where I’d put my money. If you’re hitting scaling limits or tired of patching validation and middleware, give Fastify a try. It might just save you a few weekends—and your sanity.


If you found this helpful, check out more programming tutorials on our blog. We cover Python, JavaScript, Java, Data Science, and more.

More from this blog

pythonassignmenthelp

44 posts