DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Node.js for Frontend Developers — What Nobody Explains

I spent 4 years as a frontend developer before learning Node.js.

Everything confused me at first.

Not because it's hard — but because nobody explains how it connects to what you already know.

Here's what I wish someone had told me.


The Event Loop — Finally Explained Simply

Frontend developers know JavaScript is single-threaded. But how does Node.js handle thousands of requests at the same time?

console.log('1 - Start');

setTimeout(() => {
  console.log('3 - Timeout');
}, 0);

Promise.resolve().then(() => {
  console.log('2 - Promise');
});

console.log('4 - End');

// Output:
// 1 - Start
// 4 - End
// 2 - Promise
// 3 - Timeout
Enter fullscreen mode Exit fullscreen mode

The order: synchronous code first → microtasks (promises) → macrotasks (setTimeout)

Node.js doesn't wait for slow operations. It hands them off and keeps processing. When they finish, it comes back and handles the result.

This is why Node.js can handle thousands of concurrent requests without multiple threads.


require vs import

Coming from frontend you're used to ES modules:

// Frontend (ES Modules)
import express from 'express';
import { readFile } from 'fs/promises';

// Old Node.js (CommonJS)
const express = require('express');
const { readFile } = require('fs/promises');
Enter fullscreen mode Exit fullscreen mode

In modern Node.js projects — add "type": "module" to your package.json and use ES module syntax everywhere. Or use TypeScript which handles this automatically.


The Middleware Pattern

This confused me the most coming from React.

Middleware is just a function that runs between a request and response:

import express from 'express';
const app = express();

// Middleware 1 — runs for every request
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next(); // pass to next middleware
});

// Middleware 2 — parse JSON bodies
app.use(express.json());

// Middleware 3 — auth check
function requireAuth(req, res, next) {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
}

// Route — only runs if all middleware passed
app.get('/dashboard', requireAuth, (req, res) => {
  res.json({ message: 'Welcome to dashboard' });
});
Enter fullscreen mode Exit fullscreen mode

Think of middleware as a chain. Each function either:

  • Calls next() to pass to the next one
  • Sends a response to stop the chain

Same concept as Next.js middleware — just lower level.


Environment Variables

Never hardcode secrets. Ever.

// 🚫 Never do this
const db = mongoose.connect('mongodb+srv://user:password123@cluster.mongodb.net/mydb');

// ✅ Always use environment variables
import 'dotenv/config'; // npm install dotenv
const db = mongoose.connect(process.env.MONGODB_URI);
Enter fullscreen mode Exit fullscreen mode

Create a .env file:

MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/mydb
JWT_SECRET=your-secret-key-here
PORT=3000
Enter fullscreen mode Exit fullscreen mode

Add .env to .gitignore immediately:

# .gitignore
.env
.env.local
node_modules/
Enter fullscreen mode Exit fullscreen mode

If you accidentally commit a secret — rotate it immediately. GitHub scans for exposed keys.


Async Error Handling

The most common Node.js bug for beginners:

// 🚫 Unhandled promise rejection — crashes your server
app.get('/users', async (req, res) => {
  const users = await db.find(); // if this throws — server crashes
  res.json(users);
});

// ✅ Always wrap async routes in try/catch
app.get('/users', async (req, res) => {
  try {
    const users = await db.find();
    res.json(users);
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Server error' });
  }
});

// ✅ Or create a wrapper function
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

app.get('/users', asyncHandler(async (req, res) => {
  const users = await db.find();
  res.json(users);
}));
Enter fullscreen mode Exit fullscreen mode

The asyncHandler wrapper catches all errors and passes them to Express's error handler automatically.


Reading and Writing Files

import { readFile, writeFile, readdir } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';

// Get current directory (ESM equivalent of __dirname)
const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Read a file
const content = await readFile(
  path.join(__dirname, 'data.json'),
  'utf-8'
);
const data = JSON.parse(content);

// Write a file
await writeFile(
  path.join(__dirname, 'output.json'),
  JSON.stringify(data, null, 2)
);

// List directory contents
const files = await readdir(path.join(__dirname, 'posts'));
Enter fullscreen mode Exit fullscreen mode

The path.join(__dirname, ...) pattern ensures your paths work on Windows and Mac/Linux.


A Complete Mini API

Putting it all together — a simple REST API:

// server.js
import express from 'express';
import 'dotenv/config';

const app = express();
const PORT = process.env.PORT || 3000;

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

// In-memory data (replace with database)
let users = [
  { id: 1, name: 'Anas', email: 'anas@example.com' }
];

// Routes
app.get('/api/users', (req, res) => {
  res.json({ success: true, data: users });
});

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

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

  const user = { id: Date.now(), name, email };
  users.push(user);
  res.status(201).json({ success: true, data: user });
});

app.delete('/api/users/:id', (req, res) => {
  const id = parseInt(req.params.id);
  users = users.filter(u => u.id !== id);
  res.json({ success: true });
});

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

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

Key Differences From Frontend JavaScript

Frontend Node.js
window, document Not available
fetch API Built-in from Node 18+
localStorage Use files or database
ES Modules by default CommonJS by default (add "type":"module" to change)
Runs in browser Runs on server
Single user context Many users simultaneously

Once Node.js clicked for me I could build full-stack apps without switching languages. It's worth the learning curve.

I use Node.js for all my backend work in my Next.js templates:

Get the templates: https://pixelanas.gumroad.com

What confused you most about Node.js? Drop it below 👇


Anas — full-stack developer specializing in Next.js and Node.js. X: @pixelanas

Top comments (0)