← Back to DevBytes

When to Choose Express Over Fastify

When to Choose Express Over Fastify

Node.js has no shortage of web frameworks, but two names dominate the conversation: Express, the battle-tested veteran, and Fastify, the performance-focused challenger. While Fastify often wins benchmarks and offers modern features out of the box, there are still many scenarios where Express remains the better choice. This tutorial explores when Express is the right tool for the job and how to use it effectively.

What Is Express?

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for building web and mobile applications. Released in 2010, it has become the de facto standard for Node.js server development. It is unopinionated, lightweight, and backed by an enormous ecosystem of middleware and plugins.

Fastify, by contrast, is a newer framework focused on speed and developer experience, offering built-in schema validation, JSON serialization, and a plugin architecture. However, its opinionated nature and smaller ecosystem mean it is not always the best fit.

Why the Choice Matters

Selecting the right framework affects more than just request-per-second numbers. It influences hiring, onboarding speed, third-party library compatibility, long-term maintenance, and how quickly you can ship features. Choosing Express when appropriate can save weeks of effort, while choosing it blindly can lead to performance bottlenecks. Understanding the trade-offs is essential.

Key Reasons to Choose Express Over Fastify

1. Massive Ecosystem and Middleware Support

Express has the largest middleware ecosystem of any Node.js framework. Almost every authentication library, template engine, session manager, and logging tool has first-class Express support. If your project depends on niche or legacy packages, Express is the safer bet.

const express = require('express');
const passport = require('passport');
const session = require('express-session');

const app = express();

app.use(session({ secret: 'secret-key', resave: false, saveUninitialized: false }));
app.use(passport.initialize());
app.use(passport.session());

app.get('/profile', passport.authenticate('local'), (req, res) => {
  res.json({ user: req.user });
});

app.listen(3000);

Many Passport strategies, OAuth integrations, and legacy middleware packages are designed specifically around Express's (req, res, next) signature. Adapting them to Fastify often requires compatibility layers that add complexity.

2. Familiarity and Team Onboarding

Express is likely already known by most Node.js developers. If your team is junior-heavy, rotating frequently, or working with contractors, Express flattens the learning curve. New hires can be productive on day one without learning a new plugin system, schema validation syntax, or encapsulation model.

3. Compatibility with Legacy Codebases

If you are extending or maintaining an existing Express application, introducing Fastify creates friction. Mixing frameworks within a single codebase leads to inconsistent patterns and duplicated logic. Staying with Express keeps the architecture coherent.

4. Unopinionated Flexibility

Express does not impose structure. You choose your validation library, your serialization approach, your logging tool, and your project layout. This is valuable when you have specific architectural requirements that do not align with Fastify's conventions.

const express = require('express');
const { body, validationResult } = require('express-validator');

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

app.post('/users', [
  body('email').isEmail(),
  body('password').isLength({ min: 8 })
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  res.status(201).json({ message: 'User created', data: req.body });
});

app.listen(3000);

5. Mature Documentation and Community Knowledge

Stack Overflow, blog posts, YouTube tutorials, and GitHub issues for Express span over a decade. When you hit a problem, someone has likely already solved it. Fastify's documentation is excellent, but the volume of community-generated troubleshooting content is smaller.

When Fastify Might Be the Better Choice

For balance, it is worth acknowledging where Fastify shines. If your application is a high-throughput API where every millisecond matters, Fastify's performance advantage is real. If you want built-in schema validation, automatic JSON serialization, and structured logging without adding dependencies, Fastify delivers those out of the box. For greenfield microservices with a small, experienced team, Fastify is a strong contender.

How to Use Express Effectively

Structuring Your Application

Even though Express is unopinionated, you should impose your own structure. Use express.Router() to modularize routes and keep your entry point clean.

// routes/users.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.json({ users: [] });
});

router.get('/:id', (req, res) => {
  res.json({ user: { id: req.params.id } });
});

module.exports = router;

// app.js
const express = require('express');
const usersRouter = require('./routes/users');

const app = express();
app.use(express.json());
app.use('/users', usersRouter);

app.listen(3000, () => console.log('Server running on port 3000'));

Adding Error Handling Middleware

Express requires explicit error-handling middleware. Define it last, after all other middleware and routes.

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: {
      message: err.message || 'Internal Server Error'
    }
  });
});

Integrating Async Route Handlers Safely

Express does not automatically catch errors from async handlers. Wrap them to avoid unhandled promise rejections.

const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

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

Best Practices When Using Express

require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const morgan = require('morgan');

const app = express();

app.use(helmet());
app.use(cors());
app.use(compression());
app.use(express.json());
app.use(morgan('combined'));

app.get('/health', (req, res) => res.json({ status: 'ok' }));

app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

app.listen(process.env.PORT || 3000);

Conclusion

Express remains an excellent choice when ecosystem breadth, team familiarity, legacy compatibility, and flexibility outweigh raw performance gains. For many applications — especially those relying on established middleware, staffed by teams of varying experience levels, or built on top of existing Express codebases — it is still the pragmatic and productive choice. Fastify is a fantastic framework, but choosing it purely for benchmark numbers without considering your project's real-world constraints is a mistake. Evaluate your team, your dependencies, your performance requirements, and your long-term maintenance plans. In a significant number of cases, the mature, predictable, and well-supported Express will serve you better than any faster alternative.

— Ad —

Google AdSense will appear here after approval

← Back to all articles