Node.js for Beginners: From Zero to a Working Server

Node.js revolutionized web development by making it possible to use JavaScript on the server side too. If you already know JavaScript from the browser, you’re already halfway there!

In this guide you’ll learn:

  • What Node.js is and why it’s so popular
  • How to install and configure it correctly
  • The core concepts (modules, npm, async)
  • How to build your first web server
  • Best practices and common patterns

What Node.js Is and Why You Should Learn It

Node.js is a JavaScript runtime built on Chrome’s V8 engine. It lets you run JavaScript outside the browser, making it perfect for building servers, APIs, CLI tools, and much more.

  • One Language: JavaScript on both frontend and backend
  • Performance: The ultra-fast V8 engine with non-blocking I/O
  • NPM Ecosystem: Over 2 million reusable packages
  • Scalability: Perfect for real-time applications and microservices
  • Huge Community: Tons of resources, tutorials, and support

When to Use Node.js

Great for:

  • REST and GraphQL APIs
  • Real-time applications (chat, gaming, collaboration)
  • Microservices
  • CLI tools and automation scripts
  • Server-side rendering (Next.js, Nuxt.js)

Less suited for:

  • CPU-intensive computations (machine learning, video encoding)
  • Applications that require heavy multithreading

Installation and Setup

1. Installing Node.js

Recommended Method: NVM (Node Version Manager)

NVM lets you install and manage multiple versions of Node.js:

# Install NVM (macOS/Linux)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash

# Restart your terminal, then install Node.js
nvm install --lts  # Long Term Support version
nvm use --lts

# Verify the installation
node --version  # Should show v20.x.x
npm --version   # Should show 10.x.x

Windows:

Use nvm-windows or download the installer from nodejs.org

Alternative Method (Direct Download):

Go to nodejs.org and download the LTS (Long Term Support) version.

2. Verify the Installation

# Create a test folder
mkdir node-test
cd node-test

# Create your first Node.js file
echo "console.log('Hello Node.js!');" > hello.js

# Run it
node hello.js

You should see Hello Node.js! printed in the terminal. 🎉

Node.js Core Concepts

1. Modules and require/import

Node.js uses a module system to organize code:

CommonJS (traditional):

// math.js - Export functions
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = { add, subtract };

// app.js - Import and use
const math = require('./math');

console.log(math.add(5, 3));       // 8
console.log(math.subtract(10, 4));  // 6

ES Modules (modern):

// math.mjs - Export functions
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// app.mjs - Import and use
import { add, subtract } from './math.mjs';

console.log(add(5, 3));       // 8
console.log(subtract(10, 4)); // 6

To use ES Modules in .js files, add this to package.json:

{
  "type": "module"
}

2. Built-in Modules

Node.js ships with a number of useful modules already installed:

// File system
const fs = require('fs');

// HTTP server
const http = require('http');

// Path utilities
const path = require('path');

// Operating system
const os = require('os');

console.log(os.platform());  // linux, darwin, win32
console.log(os.cpus().length); // Number of CPUs

3. NPM: Node Package Manager

NPM manages your project’s dependencies:

# Initialize a new project
npm init -y

# Install a package
npm install express

# Install a package as a dev dependency
npm install --save-dev nodemon

# Install a package globally
npm install -g typescript

Generated package.json file:

{
  "name": "my-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "express": "^4.18.0"
  },
  "devDependencies": {
    "nodemon": "^3.0.0"
  }
}

4. Asynchronous Programming

Node.js is single-threaded but non-blocking. It uses callbacks, Promises, and async/await:

❌ Synchronous Code (BLOCKING):

const fs = require('fs');

// Blocks execution until the file is read
const data = fs.readFileSync('file.txt', 'utf8');
console.log(data);
console.log('Done');

✅ Callback (OLD STYLE):

const fs = require('fs');

fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Error:', err);
    return;
  }
  console.log(data);
});

console.log('Done'); // Printed BEFORE the file's contents!

✅ Promise (MODERN):

const fs = require('fs').promises;

fs.readFile('file.txt', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error('Error:', err));

✅✅ Async/Await (CLEANEST):

const fs = require('fs').promises;

async function readMyFile() {
  try {
    const data = await fs.readFile('file.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error('Error:', err);
  }
}

readMyFile();

Your First Web Server

Simple HTTP Server (Vanilla Node.js)

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

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  // Set response headers
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  
  // Send the response
  res.end('Hello World from my Node.js server!\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Run it:

node server.js

Open your browser at http://localhost:3000 and you’ll see the message! 🚀

Server with Simple Routing

// server-routing.js
const http = require('http');

const server = http.createServer((req, res) => {
  const { url, method } = req;
  
  res.setHeader('Content-Type', 'application/json');
  
  if (url === '/' && method === 'GET') {
    res.statusCode = 200;
    res.end(JSON.stringify({ message: 'Welcome to the API' }));
  } 
  else if (url === '/users' && method === 'GET') {
    res.statusCode = 200;
    res.end(JSON.stringify([
      { id: 1, name: 'Mario' },
      { id: 2, name: 'Luigi' }
    ]));
  }
  else if (url === '/health' && method === 'GET') {
    res.statusCode = 200;
    res.end(JSON.stringify({ status: 'OK', uptime: process.uptime() }));
  }
  else {
    res.statusCode = 404;
    res.end(JSON.stringify({ error: 'Not Found' }));
  }
});

server.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Test with curl:

curl http://localhost:3000/
curl http://localhost:3000/users
curl http://localhost:3000/health
curl http://localhost:3000/notfound

Simplifying Things with Express.js

Building servers with vanilla http is fine for learning, but Express.js makes everything much simpler:

1. Setting Up a Project with Express

# Create the project folder
mkdir my-express-app
cd my-express-app

# Initialize npm
npm init -y

# Install Express
npm install express

# Install nodemon for auto-restart (dev only)
npm install --save-dev nodemon

Update package.json to add scripts:

{
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  }
}

2. Basic Express Server

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

// Middleware to parse JSON
app.use(express.json());

// GET route
app.get('/', (req, res) => {
  res.json({ message: 'Welcome to Express API' });
});

// Route with parameters
app.get('/users/:id', (req, res) => {
  const { id } = req.params;
  res.json({ userId: id, name: `User ${id}` });
});

// POST route
app.post('/users', (req, res) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email required' });
  }
  
  res.status(201).json({
    id: Date.now(),
    name,
    email,
    createdAt: new Date()
  });
});

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

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

Run in development mode:

npm run dev

Test with curl:

# GET
curl http://localhost:3000/
curl http://localhost:3000/users/123

# POST
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Mario","email":"[email protected]"}'

3. Full REST API (CRUD)

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

app.use(express.json());

// In-memory "database" (for demo purposes only)
let users = [
  { id: 1, name: 'Mario Rossi', email: '[email protected]' },
  { id: 2, name: 'Luigi Bianchi', email: '[email protected]' }
];

let nextId = 3;

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

// GET /users/:id - Get a specific 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);
});

// POST /users - Create a new 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 newUser = {
    id: nextId++,
    name,
    email
  };
  
  users.push(newUser);
  res.status(201).json(newUser);
});

// PUT /users/:id - Update a user
app.put('/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' });
  }
  
  const { name, email } = req.body;
  
  if (name) user.name = name;
  if (email) user.email = email;
  
  res.json(user);
});

// DELETE /users/:id - Delete a user
app.delete('/users/:id', (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  
  if (index === -1) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  users.splice(index, 1);
  res.status(204).send();
});

app.listen(3000, () => {
  console.log('CRUD API running on http://localhost:3000');
});

Full test run:

# Create a user
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Anna Verdi","email":"[email protected]"}'

# List users
curl http://localhost:3000/users

# Get a specific user
curl http://localhost:3000/users/1

# Update a user
curl -X PUT http://localhost:3000/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Mario Rossi Jr."}'

# Delete a user
curl -X DELETE http://localhost:3000/users/2

Working with Files

Reading Files

const fs = require('fs').promises;
const path = require('path');

async function readConfig() {
  try {
    const filePath = path.join(__dirname, 'config.json');
    const data = await fs.readFile(filePath, 'utf8');
    const config = JSON.parse(data);
    console.log(config);
  } catch (err) {
    console.error('Error reading file:', err);
  }
}

readConfig();

Writing Files

const fs = require('fs').promises;

async function saveUserData(user) {
  try {
    const data = JSON.stringify(user, null, 2);
    await fs.writeFile('user.json', data, 'utf8');
    console.log('File saved successfully');
  } catch (err) {
    console.error('Error writing file:', err);
  }
}

saveUserData({ 
  name: 'Mario', 
  email: '[email protected]' 
});

Creating Directories

const fs = require('fs').promises;

async function createUploadsFolder() {
  try {
    await fs.mkdir('uploads', { recursive: true });
    console.log('Folder created');
  } catch (err) {
    console.error('Error:', err);
  }
}

createUploadsFolder();

Environment Variables

DO NOT hardcode sensitive configuration! Use environment variables instead:

1. Install dotenv

npm install dotenv

2. Create a .env File

PORT=3000
DB_HOST=localhost
DB_USER=admin
DB_PASSWORD=supersecret
API_KEY=abc123xyz
NODE_ENV=development

⚠️ IMPORTANT: Add .env to .gitignore!

# .gitignore
node_modules/
.env

3. Use It in Your Code

// server.js
require('dotenv').config();

const express = require('express');
const app = express();

const port = process.env.PORT || 3000;
const dbHost = process.env.DB_HOST;
const apiKey = process.env.API_KEY;

app.get('/config', (req, res) => {
  res.json({
    environment: process.env.NODE_ENV,
    port: port,
    dbHost: dbHost
    // DO NOT expose API_KEY or passwords!
  });
});

app.listen(port, () => {
  console.log(`Server on port ${port} in ${process.env.NODE_ENV} mode`);
});

Error Handling Best Practices

1. Try-Catch for Async Functions

app.get('/users/:id', async (req, res) => {
  try {
    const user = await db.getUserById(req.params.id);
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    res.json(user);
  } catch (err) {
    console.error('Error fetching user:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

2. Global Error Middleware

// Error-handling middleware (ALWAYS goes last)
app.use((err, req, res, next) => {
  console.error(err.stack);
  
  res.status(err.status || 500).json({
    error: {
      message: err.message || 'Internal Server Error',
      ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
  });
});

3. Handling Async Errors

// Wrapper to avoid repeating try-catch everywhere
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Usage
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.getUserById(req.params.id);
  res.json(user);
}));

Common Mistakes and How to Avoid Them

1. Forgetting to Handle Async Errors

WRONG:

app.get('/data', async (req, res) => {
  const data = await fetchData(); // If it fails, crash!
  res.json(data);
});

RIGHT:

app.get('/data', async (req, res) => {
  try {
    const data = await fetchData();
    res.json(data);
  } catch (err) {
    res.status(500).json({ error: 'Failed to fetch data' });
  }
});

2. Exposing Sensitive Information

WRONG:

app.get('/debug', (req, res) => {
  res.json({
    dbPassword: process.env.DB_PASSWORD,
    apiKey: process.env.API_KEY
  });
});

RIGHT:

app.get('/health', (req, res) => {
  res.json({
    status: 'OK',
    environment: process.env.NODE_ENV,
    uptime: process.uptime()
    // No secrets!
  });
});

3. Not Validating Input

WRONG:

app.post('/users', (req, res) => {
  const user = req.body; // Accepts anything!
  users.push(user);
  res.json(user);
});

RIGHT:

app.post('/users', (req, res) => {
  const { name, email } = req.body;
  
  if (!name || typeof name !== 'string') {
    return res.status(400).json({ error: 'Invalid name' });
  }
  
  if (!email || !email.includes('@')) {
    return res.status(400).json({ error: 'Invalid email' });
  }
  
  const user = { id: nextId++, name, email };
  users.push(user);
  res.status(201).json(user);
});

4. Blocking the Event Loop

WRONG:

app.get('/heavy', (req, res) => {
  // Synchronous, CPU-intensive computation
  let result = 0;
  for (let i = 0; i < 10000000000; i++) {
    result += i;
  }
  res.json({ result });
});

RIGHT:

const { Worker } = require('worker_threads');

app.get('/heavy', (req, res) => {
  const worker = new Worker('./heavy-calc.js');
  
  worker.on('message', (result) => {
    res.json({ result });
  });
  
  worker.on('error', (err) => {
    res.status(500).json({ error: 'Calculation failed' });
  });
});

For larger projects, organize your code like this:

my-app/
├── src/
│   ├── controllers/
│   │   └── userController.js
│   ├── models/
│   │   └── User.js
│   ├── routes/
│   │   └── userRoutes.js
│   ├── middleware/
│   │   └── auth.js
│   ├── utils/
│   │   └── logger.js
│   └── app.js
├── tests/
│   └── user.test.js
├── .env
├── .env.example
├── .gitignore
├── package.json
└── server.js

Example server.js:

require('dotenv').config();
const app = require('./src/app');

const port = process.env.PORT || 3000;

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Example src/app.js:

const express = require('express');
const userRoutes = require('./routes/userRoutes');

const app = express();

app.use(express.json());
app.use('/api/users', userRoutes);

module.exports = app;

Next Steps

Now that you have the basics down, you can explore:

  1. Database: Connect PostgreSQL, MongoDB, or MySQL
  2. Authentication: Implement JWT or session-based auth
  3. Testing: Write tests with Jest or Mocha
  4. Deployment: Deploy to Heroku, Vercel, or AWS
  5. Frameworks: Try NestJS, Fastify, or Koa
  6. Real-time: Implement WebSocket with Socket.io

Useful Resources

Conclusion

You just learned:

✅ What Node.js is and how to install it
✅ The core concepts (modules, npm, async)
✅ How to build HTTP and Express servers
✅ A complete CRUD REST API
✅ Best practices and mistakes to avoid

Node.js opens up a world of possibilities. With practice, you’ll be able to build scalable APIs, automation tools, and real-time applications.

Keep following Code Fortress for more advanced tutorials on backend development, security, and DevOps!


Have questions about Node.js? Comment below or follow us on social media. Happy coding! 🚀