DEV Community

sisproid
sisproid

Posted on

Hey! I Just Discovered Something Pretty Cool About ExpressJS

You know that feeling when something just clicks? I had one of those moments with ExpressJS recently. Been building APIs the "standard" way for years, but stumbled onto this minimalist approach that's honestly kind of refreshing. Want to see what I mean?

What's This All About?

What You'll Need

Nothing fancy:

  • Node.js (pretty much any recent version)
  • Whatever editor you're comfortable with
  • Maybe 15 minutes if you want to try it out
  • The usual curiosity we all have as developers

Time to read: About 10 minutes

Complexity: Pretty straightforward

The Thing That Got Me Thinking

So last week, I'm looking at this Express app I built a few months ago. You know how it goes - started simple, then gradually added stuff as requirements came in. Before I knew it, I had this package.json with like 30+ dependencies.

The app was taking forever to start up, and honestly, I couldn't remember what half the packages were even doing anymore. Sound familiar?

That's when I thought: "What if I just... stripped it back to basics?"

What I noticed:

  • Faster to work with
  • Way easier to understand what's happening
  • Less stuff to break
  • Debugging became actually enjoyable again

My Little Discovery Story

I decided to experiment. What's the absolute minimum I need for a working API?

Turns out, it's pretty minimal:

// Literally just this
const express = require('express');
const app = express();

app.get('/hello', (req, res) => {
  res.json({ message: 'Hey there!' });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
Enter fullscreen mode Exit fullscreen mode

I know, I know - "But what about middleware? What about security? What about all the best practices?"

Here's the thing though: this little guy actually works really well for a lot of use cases. And when you do need to add stuff, you actually understand why you're adding it.

Let Me Show You What I Mean

Starting Simple

Instead of the usual massive package.json, try this:

{
  "name": "simple-api",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js",
    "dev": "node --watch server.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

Quick note: That --watch flag is built into newer Node versions. No need for nodemon unless you specifically want it.

Building Something Useful

Let's make a basic user API. Nothing fancy, but something you'd actually use:

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

// Just the basics
app.use(express.json());

// Some sample data to work with
let users = [
  { id: 1, name: 'Sarah', email: 'sarah@example.com' },
  { id: 2, name: 'Mike', email: 'mike@example.com' }
];
let nextId = 3;

// Simple health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', time: new Date().toISOString() });
});

// Get all users
app.get('/users', (req, res) => {
  res.json(users);
});

// Get one user
app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

// Add a user
app.post('/users', (req, res) => {
  const { name, email } = req.body;

  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email required' });
  }

  const user = { id: nextId++, name, email };
  users.push(user);
  res.status(201).json(user);
});

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

What I like about this: You can see everything that's happening. No magic, no hidden complexity.

Adding a Bit More

Once you get comfortable, you might want to add some basic features:

// Simple logging (because it's nice to see what's happening)
app.use((req, res, next) => {
  console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
  next();
});

// Update a user
app.put('/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === id);

  if (userIndex === -1) {
    return res.status(404).json({ error: 'User not found' });
  }

  users[userIndex] = { ...users[userIndex], ...req.body, id };
  res.json(users[userIndex]);
});

// Delete a user
app.delete('/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === id);

  if (userIndex === -1) {
    return res.status(404).json({ error: 'User not found' });
  }

  users.splice(userIndex, 1);
  res.status(204).send();
});
Enter fullscreen mode Exit fullscreen mode

The Whole Thing Put Together

Here's what the complete version looks like:

// server.js - Everything in one place
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Basic middleware
app.use(express.json());
app.use(express.static('public')); // if you have static files

// Simple request logging
app.use((req, res, next) => {
  console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
  next();
});

// Our data (in a real app, this might be a database)
let users = [
  { id: 1, name: 'Sarah Chen', email: 'sarah@example.com' },
  { id: 2, name: 'Mike Johnson', email: 'mike@example.com' }
];
let nextId = 3;

// Routes
app.get('/health', (req, res) => {
  res.json({ 
    status: 'ok', 
    timestamp: new Date().toISOString(),
    uptime: Math.floor(process.uptime())
  });
});

app.get('/users', (req, res) => {
  res.json({ users, count: users.length });
});

app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

app.post('/users', (req, res) => {
  const { name, email } = req.body;

  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }

  // Basic email validation
  if (!email.includes('@')) {
    return res.status(400).json({ error: 'Please provide a valid email' });
  }

  const user = { id: nextId++, name, email, createdAt: new Date() };
  users.push(user);
  res.status(201).json(user);
});

app.put('/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === id);

  if (userIndex === -1) {
    return res.status(404).json({ error: 'User not found' });
  }

  users[userIndex] = { ...users[userIndex], ...req.body, id, updatedAt: new Date() };
  res.json(users[userIndex]);
});

app.delete('/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === id);

  if (userIndex === -1) {
    return res.status(404).json({ error: 'User not found' });
  }

  users.splice(userIndex, 1);
  res.status(204).send();
});

// Handle 404s
app.use('*', (req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

// Basic error handling
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong' });
});

// Start the server
app.listen(PORT, () => {
  console.log(`\nServer running on http://localhost:${PORT}`);
  console.log('Available endpoints:');
  console.log('  GET    /health');
  console.log('  GET    /users');
  console.log('  POST   /users');
  console.log('  GET    /users/:id');
  console.log('  PUT    /users/:id');
  console.log('  DELETE /users/:id');
  console.log('');
});
Enter fullscreen mode Exit fullscreen mode

Want to Try It?

Pretty straightforward:

  1. mkdir my-simple-api && cd my-simple-api
  2. Save the code as server.js
  3. npm init -y && npm install express
  4. node server.js
  5. Open http://localhost:3000/health

That's it. You've got a working API.

Some Things I Learned the Hard Way

Don't Install Everything Upfront

I used to do this:

# My old habit
npm install express helmet cors compression morgan joi bcrypt jsonwebtoken passport
Enter fullscreen mode Exit fullscreen mode

Now I do this:

# Start simple
npm install express
# Add stuff when I actually need it
Enter fullscreen mode Exit fullscreen mode

Why this works better: You understand each piece as you add it, instead of trying to configure 10 things at once.

One File Isn't Always Bad

Yeah, I know. We're taught to split everything into modules from day one. But sometimes a single file is actually clearer:

// This is fine for small to medium APIs
// server.js with everything in one place

// When it gets unwieldy (maybe 200+ lines), then split it up
// routes/users.js
// middleware/auth.js
// etc.
Enter fullscreen mode Exit fullscreen mode

The thing is: Let the code tell you when it wants to be split up, don't do it preemptively.

Basic Validation Often Does the Job

Instead of bringing in a whole validation library:

// ❌ Sometimes overkill
const Joi = require('joi');
const schema = Joi.object({
  name: Joi.string().required(),
  email: Joi.string().email().required()
});

// ✅ Often sufficient
if (!name || !email || !email.includes('@')) {
  return res.status(400).json({ error: 'Invalid input' });
}
Enter fullscreen mode Exit fullscreen mode

When to upgrade: When your validation logic gets complex or you're repeating yourself a lot.

A Few Interesting Numbers

I did some basic testing on this approach vs. a more "standard" Express setup:

Startup Time

  • Simple version: ~20ms
  • Full middleware stack: ~200-500ms

Memory Usage

  • Simple version: ~30MB
  • With lots of middleware: ~80-150MB

Development Time

  • Getting started: Way faster (obviously)
  • Adding new features: Also faster (less stuff to work around)
  • Debugging: Much easier (fewer moving parts)

Note: These aren't scientific benchmarks, just what I noticed in my day-to-day work.

Other Ways You Could Do This

If You Want Even More Speed

Fastify is worth looking at:

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

fastify.get('/users', async (request, reply) => {
  return { users: [] };
});

fastify.listen({ port: 3000 });
Enter fullscreen mode Exit fullscreen mode

When I use this: When performance is really important and I don't mind a slightly different API.

If You Want Zero Dependencies

Pure Node.js HTTP module:

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
  const { pathname } = url.parse(req.url);

  if (pathname === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok' }));
    return;
  }

  res.writeHead(404);
  res.end('Not found');
});

server.listen(3000);
Enter fullscreen mode Exit fullscreen mode

When I use this: Really small microservices where every millisecond matters.

Where This Actually Works

Quick Prototypes

Perfect when you're testing an idea and don't want to get bogged down in setup.

Internal Tools

APIs that only your team uses. Less complexity = easier maintenance.

Learning Projects

When you're trying to understand how something works, fewer layers make it clearer.

Small Production Services

I've got a few simple APIs in production using this approach. They just work.

Where it might not fit: Large, complex applications with lots of business rules. But even then, starting simple and growing organically often works better than big design upfront.

What I Think About It All

Look, I'm not saying this is the only way to build APIs. There's definitely a place for more structured, enterprise-y approaches.

But I've found that starting minimal has some real benefits:

You understand your code better because there's less magic happening behind the scenes.

Debugging is easier when you can see the whole request flow in one place.

Adding complexity becomes intentional instead of just following a template.

New team members get up to speed faster because there's less to learn upfront.

The main thing I've learned: it's okay to start simple. You can always add complexity later when you actually need it.

Some Stuff You Might Find Useful

Documentation

Tools That Help

  • Node.js --watch flag - Built-in file watching
  • VS Code REST Client - Test your APIs without switching apps
  • Postman/Insomnia - When you need more advanced API testing

Things to Read

  • "You Aren't Gonna Need It" (YAGNI principle) - Changed how I think about features
  • Any article about dependency management - Helpful for understanding the tradeoffs

About Me

Just a regular developer who's been building web stuff for a while. I like finding simpler ways to do things, mostly because I get tired of fighting with complicated toolchains. Always happy to chat about this stuff if you have questions or your own experiences to share.

Found this useful? Feel free to share it with other devs who might be interested. Or if you try this approach, I'd love to hear how it goes!


Last updated: September 2025 | Written while drinking too much coffee and enjoying simple code

Top comments (0)