Skip to main content
Knowledge Hub

Building APIs with Raw Node.js

Creating backend APIs using only Node.js core modules without frameworks

Last updated: May 8, 2025

While frameworks like Express make building Node.js applications easier, understanding how to build applications using only Node.js core modules provides deep insight into how Node.js works and what frameworks abstract away.

Building with raw Node.js means using only built-in modules like http, https, fs, crypto, and url, without external frameworks or libraries.

Why Build with Raw Node.js

Building with raw Node.js helps you understand the fundamentals. You learn exactly how HTTP requests are handled, how routing works, and how middleware patterns are implemented.

It gives you full control over every aspect of request handling. You’re not limited by framework conventions or abstractions.

Understanding raw Node.js makes you a better framework user. You understand what frameworks do under the hood and can make better decisions about when to use frameworks versus when to build custom solutions.

Core Modules

Node.js provides several core modules essential for building web applications. The http module creates HTTP servers and handles requests. The https module does the same for HTTPS connections.

The url module parses URLs to extract paths, query parameters, and other components. The fs module handles file system operations for reading and writing data. The crypto module provides cryptographic functions for hashing passwords and generating tokens.

Creating an HTTP Server

The basic structure of a raw Node.js server:

const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url, true);
  const path = parsedUrl.pathname;
  const method = req.method;
  
  // Route handling logic here
  
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: 'Hello World' }));
});

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

The createServer method takes a callback that receives request and response objects. The callback runs for every incoming request.

Parsing Requests

Raw Node.js requires manual parsing of request data. For URL parsing:

const parsedUrl = url.parse(req.url, true);
const path = parsedUrl.pathname;
const query = parsedUrl.query;

For request bodies, you need to collect data chunks:

let body = '';
req.on('data', (chunk) => {
  body += chunk.toString();
});

req.on('end', () => {
  const data = JSON.parse(body);
  // Process data
});

This manual parsing is what frameworks like Express handle automatically.

Implementing Routing

Routing requires manually matching paths and methods:

const routes = {
  'GET': {
    '/': homeHandler,
    '/users': usersHandler,
    '/users/:id': userHandler
  },
  'POST': {
    '/users': createUserHandler
  }
};

const handler = routes[method]?.[path];
if (handler) {
  handler(req, res);
} else {
  res.writeHead(404);
  res.end('Not Found');
}

For dynamic routes, you need to implement pattern matching:

function matchRoute(pattern, path) {
  const regex = new RegExp('^' + pattern.replace(/:\w+/g, '([^/]+)') + '$');
  const match = path.match(regex);
  return match ? match.slice(1) : null;
}

Handling Request Bodies

Parsing JSON request bodies manually:

function parseBody(req) {
  return new Promise((resolve, reject) => {
    let body = '';
    req.on('data', (chunk) => {
      body += chunk.toString();
    });
    req.on('end', () => {
      try {
        resolve(JSON.parse(body));
      } catch (err) {
        reject(err);
      }
    });
  });
}

This is what express.json() middleware does automatically.

Response Helpers

Create helper functions for common response patterns:

function sendJSON(res, data, statusCode = 200) {
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
}

function sendError(res, message, statusCode = 500) {
  sendJSON(res, { error: message }, statusCode);
}

These helpers reduce repetition and make code more maintainable.

File-Based Data Storage

Without a database, you can use the file system for data storage:

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

async function readData(fileName) {
  try {
    const data = await fs.readFile(
      path.join(__dirname, '.data', fileName),
      'utf8'
    );
    return JSON.parse(data);
  } catch (err) {
    return null;
  }
}

async function writeData(fileName, data) {
  await fs.mkdir(path.join(__dirname, '.data'), { recursive: true });
  await fs.writeFile(
    path.join(__dirname, '.data', fileName),
    JSON.stringify(data),
    'utf8'
  );
}

This approach works for simple applications but doesn’t scale well.

Password Hashing

Use the crypto module for password hashing:

const crypto = require('crypto');

function hashPassword(password) {
  const salt = crypto.randomBytes(16).toString('hex');
  const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
  return salt + ':' + hash;
}

function verifyPassword(password, hashedPassword) {
  const [salt, hash] = hashedPassword.split(':');
  const verifyHash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
  return hash === verifyHash;
}

Modular Architecture

Organize code into modules even without a framework:

project/
  index.js           # Server entry point
  lib/
    server.js        # HTTP server setup
    data.js         # Data storage
  handlers/
    userHandler.js   # User operations
    tokenHandler.js  # Token management
  helpers/
    utilities.js    # Utility functions
  routes.js         # Route definitions

This modular structure makes code maintainable and testable.

Advantages and Limitations

Building with raw Node.js provides deep understanding and full control, but requires more code and manual handling of common tasks.

Frameworks exist for good reasons: they handle common patterns, provide security features, and reduce boilerplate. For production applications, frameworks are usually the better choice.

However, understanding raw Node.js makes you appreciate what frameworks do and helps you use them more effectively.

When to Use Raw Node.js

Use raw Node.js for learning, simple applications, or when you need maximum control. For most production applications, use a framework like Express, which provides the abstractions you need without the overhead of building everything from scratch.

Summary

Building with raw Node.js teaches you the fundamentals of how web servers work. While frameworks make development easier, understanding the underlying mechanisms makes you a better developer.

The key is understanding request parsing, routing, response handling, and how to organize code without framework conventions. This knowledge helps you use frameworks more effectively and build custom solutions when needed.