CS 106 structured Week 6

Node.js and Async Programming

Node.js lets you run JavaScript on the server. Unlike a browser, where JavaScript runs one interaction at a time, Node.js is designed to handle many things simultaneously without blocking — this is called asynchronous programming.

Why Async Matters

Most server operations take time:

  • Reading a file from disk
  • Querying a database
  • Fetching data from another API
  • Sending an email

Synchronous code waits for each operation to finish before moving to the next. Asynchronous code starts the operation and moves on, coming back when the result is ready. This is what makes Node.js fast.

Promises

A Promise represents a value that will be available in the future. It exists in one of three states:

State Meaning
Pending The operation is in progress
Fulfilled The operation completed successfully
Rejected The operation failed
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Data loaded');
  }, 2000);
});

promise
  .then((result) => console.log(result))   // runs on success
  .catch((error) => console.error(error))  // runs on failure
  .finally(() => console.log('Done'));     // always runs

Async / Await

async/await is the modern way to write asynchronous code. Under the hood it still uses Promises, but the syntax is cleaner and easier to read:

async function fetchUser(id) {
  try {
    const response = await fetch(`https://api.example.com/users/${id}`);
    const user = await response.json();
    console.log(user);
    return user;
  } catch (error) {
    console.error('Failed to fetch user:', error);
  }
}

fetchUser(1);

await pauses execution inside the function until the Promise resolves. async marks a function as one that contains await. Always wrap await calls in try/catch to handle errors.

Common Async Patterns

Multiple requests in parallel

async function loadDashboard() {
  // These run at the same time, not one after another
  const [users, products, orders] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/products').then(r => r.json()),
    fetch('/api/orders').then(r => r.json()),
  ]);

  return { users, products, orders };
}

Reading files

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

async function readConfig() {
  try {
    const content = await fs.readFile('config.json', 'utf8');
    return JSON.parse(content);
  } catch (error) {
    console.error('Could not read config:', error);
  }
}

Building a Simple HTTP Server

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: 'Hello from Node.js' }));
});

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

Express — A Minimal Web Framework

Express makes building APIs much simpler:

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

app.use(express.json()); // parse incoming JSON

// GET request
app.get('/users', async (req, res) => {
  const users = await getUsers(); // your database function
  res.json(users);
});

// POST request
app.post('/users', async (req, res) => {
  const { name, email } = req.body;
  const user = await createUser({ name, email });
  res.status(201).json(user);
});

// DELETE request
app.delete('/users/:id', async (req, res) => {
  await deleteUser(req.params.id);
  res.status(204).send();
});

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

Environment Variables

Never hardcode secrets. Use environment variables:

// .env file (never commit this)
// DATABASE_URL=postgres://...
// API_KEY=abc123

require('dotenv').config();

const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;
npm install dotenv

Add .env to your .gitignore.

Logging

Good logging is essential for debugging in production:

// Basic — fine for small projects
console.log('Server started');
console.error('Something failed:', error);
console.table(users); // renders arrays/objects as a table

// For larger projects, use a logging library like Winston
const winston = require('winston');
const logger = winston.createLogger({
  level: 'info',
  transports: [new winston.transports.Console()],
});

logger.info('User created', { userId: 123 });
logger.error('Database connection failed', { error: err.message });

Good log messages include:

  • What happened
  • Relevant data (user ID, request path)
  • When it happened (timestamps)

Common npm Commands

npm init -y              # create a new package.json
npm install express      # install a package
npm install -D nodemon   # install a dev-only package
npm run dev              # run your dev script
npx nodemon index.js     # restart server automatically on file changes

Project Structure

A clean Node/Express project looks like:

my-api/
├── src/
│   ├── routes/
│   │   ├── users.js
│   │   └── products.js
│   ├── controllers/
│   ├── middleware/
│   └── index.js
├── .env
├── .gitignore
└── package.json

Keep routes thin. Business logic goes in controllers. Shared checks (authentication, validation) go in middleware.