Back to Node.js tutorials
Basic17 min read

Node.js Fundamentals

Understand the Node.js runtime, module systems, npm workflows, and environment configuration for server-side JavaScript.

The Node.js Runtime

Node.js executes JavaScript on the server using the V8 engine wrapped in a C++ runtime that provides file system, networking, and process APIs. It uses a single-threaded event loop with a libuv thread pool for I/O, making it efficient for I/O-bound workloads.

Unlike browsers, Node has no DOM. Instead you work with modules like fs, http, path, and crypto. Understanding non-blocking I/O is essential: callbacks, Promises, or async/await release the thread while waiting on disk or network.

Install Node via nvm or official installers so you can pin LTS versions per project. Check node -v and npm -v before starting tutorials.

  • Prefer Active LTS releases for production services
  • Use package.json engines field to document required Node version
  • Monitor memory usage; CPU-heavy tasks may need worker threads
node -v
npm -v
node -e "console.log(process.version)"

CommonJS Modules

Traditional Node modules use require() to import and module.exports or exports to export. Each file is wrapped in a function scope, so variables do not leak globally unless assigned to global.

require resolves paths relative to the current file, then node_modules. The module cache means require returns the same instance on subsequent imports.

CommonJS remains widespread in older codebases. New projects often use ES modules, but you must recognize require in maintenance work.

  • Avoid circular requires by extracting shared types to a third module
  • Use __dirname and __filename in CommonJS for path resolution
  • Default export pattern: module.exports = class User {}
// logger.js
module.exports = { info: (msg) => console.log('[info]', msg) };

// app.js
const logger = require('./logger');
logger.info('Server starting');

npm and package.json

npm manages dependencies declared in package.json. Run npm init to scaffold a project and npm install express to add packages. Lockfiles (package-lock.json) pin transitive versions for reproducible installs.

Scripts automate tasks: "start", "dev", "test". Use npm run dev with tools like nodemon or tsx for local development.

Distinguish dependencies from devDependencies. Production deploys should omit dev packages with npm ci --omit=dev in CI pipelines.

  • Commit lockfiles to version control
  • Audit dependencies with npm audit regularly
  • Use exact versions for critical security libraries when needed
{
  "name": "api-server",
  "type": "module",
  "scripts": { "start": "node src/index.js", "dev": "node --watch src/index.js" },
  "dependencies": { "express": "^4.21.0" }
}

Environment Variables

process.env exposes environment variables as strings. Load local .env files with dotenv in development, but inject secrets from your hosting platform in production—never commit .env with credentials.

Validate configuration at startup with libraries like zod or envalid. Fail fast when required variables are missing rather than crashing mid-request.

Use NODE_ENV=production to enable performance optimizations in frameworks and logging libraries.

  • Document required env vars in README or .env.example
  • Rotate secrets without redeploying code when platform supports it
  • Avoid logging secrets in error messages
import 'dotenv/config';

const port = Number(process.env.PORT ?? 3000);
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL required');

Get In Touch


Ready to discuss your next project? Drop me a message.