Server Runtime

Node.js Basics

Node.js runs JavaScript outside the browser, on the server. Its non-blocking I/O and huge ecosystem make it a natural choice for APIs, tools and full-stack apps.

beginner15 min readUpdated Sep 15, 2026
server.js
js
// server.js
import { createServer } from "node:http";

const server = createServer((req, res) => {
  if (req.url === "/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ status: "ok" }));
    return;
  }
  res.writeHead(404).end("Not found");
});

server.listen(3000, () => {
  console.log("http://localhost:3000");
});
First release
2009, by Ryan Dahl
Engine
V8
Model
Non-blocking, event-driven
Modules
ESM and CommonJS
Packages
npm registry
Great for
APIs, tools, real-time apps

Why it matters

Why Node.js matters

JavaScript on the server

Use the same language on both sides of the wire, which simplifies teams, tooling and code sharing.

Non-blocking I/O

The event loop keeps handling requests while waiting on files, networks and databases, so throughput stays high.

A massive ecosystem

npm provides packages for nearly every task, and frameworks like Express and Fastify make building APIs quick.

The big picture

The three ideas behind Node.js

A JavaScript engine, a non-blocking event loop and a standard library for files, networks and processes.

The runtime

Execute

V8 plus the Node APIs for files, networking, processes and timers.

The event loop

Schedule

One thread, a callback queue and non-blocking system calls.

The ecosystem

Extend

The npm registry supplies frameworks, libraries and command-line tools.

Node.js at a glance

The core of Node.js

HTTP server

Build an API with the built-in http module or a framework.

File system

Read and write files with the async fs API.

process and env

Read arguments, environment variables and exit codes.

Events and streams

Handle data in chunks and respond to events as they occur.

Modules

Organise code with ESM import and export.

APIs and tools

Build REST APIs, GraphQL servers, CLIs and background workers.

A short history

JavaScript escapes the browser

  1. 2009

    Node.js released

    Ryan Dahl runs JavaScript on the server with an event-driven model.

    09
  2. 2011

    npm and Express

    A package manager and a web framework accelerate adoption.

    11
  3. 2015

    Long-term support

    LTS releases and a foundation bring stability to production use.

    15
  4. 2017

    async/await

    Asynchronous server code becomes far easier to read.

    17
  5. Today

    Everywhere

    The default runtime for build tools, APIs, serverless functions and CLIs.

    Today

The complete guide

Node.js Basics: Everything you need to know

Why Node.js matters

Node.js is a runtime that runs JavaScript outside the browser. Released in 2009, it took the V8 engine from Chrome and paired it with APIs for files, networking and processes, letting developers use one language across the whole stack.

Its defining characteristic is non-blocking, event-driven I/O. Instead of reserving a thread per request and waiting while the disk or network responds, Node hands off the slow operation and continues with other work. When the operation finishes, a callback runs. That model makes it efficient for APIs, real-time apps and tools, and it is the same event loop you already met in browser JavaScript.

Running JavaScript on the server

Node gives you a global process, module system and standard library. A minimal HTTP server needs no dependencies.

// server.js
import { createServer } from "node:http";

const server = createServer((req, res) => {
  if (req.url === "/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ status: "ok" }));
    return;
  }
  res.writeHead(404).end("Not found");
});

server.listen(process.env.PORT ?? 3000);

The built-in node: prefixed imports make it clear which modules come from Node itself. This server handles requests as they arrive and never blocks on I/O.

Modules

Modern Node uses ES modules, the same import and export syntax as the browser.

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

// app.js
import { add } from "./math.js";

Set "type": "module" in package.json to treat .js files as ES modules. Node also still supports CommonJS (require and module.exports), which you will encounter in older code and many packages. New projects should use ES modules.

The file system

Node’s file system API comes in async and sync variants. Use the async API in servers.

// files.js
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";

const file = path.join(import.meta.dirname, "data.json");
const data = JSON.parse(await readFile(file, "utf8"));

await writeFile(file, JSON.stringify({ ...data, updated: Date.now() }, null, 2));

The node:path module handles cross-platform path joining, and import.meta.dirname gives the current file’s directory. Sync functions like readFileSync exist and are fine in scripts and startup code, but never inside a request handler.

Environment and configuration

Configuration belongs in environment variables, not in code.

// env.js
const config = {
  port: Number(process.env.PORT ?? 3000),
  databaseUrl: process.env.DATABASE_URL,
  nodeEnv: process.env.NODE_ENV ?? "development",
};

This keeps secrets out of the repository and lets the same build run in development, staging and production. Load local values from a .env file in development with a tool like dotenv or Node’s built-in --env-file, and never commit that file.

Building APIs

Most Node apps use a framework to handle routing, parsing and middleware. Express is the most widely used; Fastify is faster and schema-oriented.

// api.js
import express from "express";

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

app.get("/api/users", async (req, res, next) => {
  try {
    const users = await db.users.findMany();
    res.json(users);
  } catch (error) {
    next(error);
  }
});

app.post("/api/users", async (req, res, next) => {
  try {
    const user = await db.users.create(req.body);
    res.status(201).json(user);
  } catch (error) {
    next(error);
  }
});

app.use((error, req, res, next) => {
  console.error(error);
  res.status(500).json({ message: "Something went wrong" });
});

app.listen(config.port);

Route handlers are async functions, errors are forwarded to middleware, and a final error handler turns exceptions into responses. Return meaningful status codes and headers, and see Web Security for validating input and protecting sessions.

Streams, buffers and events

Node handles data as streams so large payloads do not have to fit in memory.

// stream.js
import { createReadStream } from "node:fs";
import { createServer } from "node:http";

createServer((req, res) => {
  createReadStream("movie.mp4").pipe(res);
}).listen(3000);

A buffer is raw binary data, and an event emitter lets objects publish events that others subscribe to. Streams and events are the low-level machinery behind much of Node’s standard library, and understanding them explains how files, sockets and HTTP work under the hood.

Errors and process signals

Unhandled errors can crash a Node process, so handle them deliberately.

// signals.js
process.on("SIGTERM", () => {
  server.close(() => process.exit(0));
});

process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection", reason);
});

SIGTERM is sent by most platforms when a container or process should stop; handling it lets you finish in-flight requests before exiting. Global handlers are a safety net for logging, not a substitute for handling errors where they occur.

Best practices

  • Use async APIs in request handlers and never block the event loop.
  • Keep configuration in environment variables.
  • Validate and sanitise all input on the server.
  • Use ES modules and the node: prefix for built-ins.
  • Centralise error handling in middleware.
  • Stream large files instead of reading them into memory.
  • Handle SIGTERM for graceful shutdown and expose a health endpoint.

Common mistakes

  • Using readFileSync inside a request handler and stalling the server.
  • Doing CPU-heavy work on the main thread.
  • Committing .env files or hardcoding secrets.
  • Trusting client input without validation.
  • Ignoring unhandled promise rejections.
  • Reading large files entirely into memory instead of streaming.

Where to go next

Node.js completes the full-stack picture. Manage dependencies with npm, understand the HTTP protocol your server speaks, secure it with Web Security, and package it with Docker. Then build a small API with two routes, a database call and an error handler, and deploy it.

Reading files

Use the async API so the event loop stays free. Blocking calls stall every other request while they wait.

Prefer
import { readFile } from "node:fs/promises";

const data = await readFile("config.json", "utf8");
const config = JSON.parse(data);
Avoid
import { readFileSync } from "node:fs";

// blocks the whole server
// while the disk responds
const data = readFileSync("config.json", "utf8");

Configuration

Read secrets and environment-specific values from environment variables so the same image runs in every environment.

Prefer
const port = process.env.PORT ?? 3000;
const dbUrl = process.env.DATABASE_URL;
Avoid
const port = 3000;
const dbUrl = "postgres://localhost/prod";
// committed credentials

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Node.js Basics?

Our interactive tutorial walks you through Node.js Basics step by step — with quizzes and real code you can run in the browser.