DEV Community

Armand al-farizy
Armand al-farizy

Posted on

Under the Hood: Building a RESTful API from Scratch with Vanilla Node.js

Introduction

We live in an era of npm install magic-framework. If a modern developer needs to spin up a backend API, the immediate instinct is to install Express, NestJS, or Hapi. While these frameworks are incredible for productivity, they act as massive black boxes.

When a complex network bug occurs, or when a memory leak happens in the routing layer, developers who only know the framework often hit a wall. They understand how to use the tool, but they don't understand how the machine actually works.

To truly master backend engineering, you need to understand what happens underneath the framework. In this article, we are going to ditch the external dependencies. We will build a fully functional CRUD RESTful API using nothing but Vanilla Node.js and its core http module.

The Challenge of a Framework-Less Server

When you use a framework like Express, handling a route is as simple as app.get('/notes', handler).

Without a framework, Node.js doesn't know what a "route" is. It only gives you a raw incoming Request stream and an outgoing Response object. You have to manually:

  1. Parse the requested URL.
  2. Check the HTTP method (GET, POST, PUT, DELETE).
  3. Manually collect incoming data buffers (because data over the internet doesn't arrive all at once; it arrives in chunks).

Let's build a simple API to manage a notebook application to see this in action.

Step 1: The Raw HTTP Server and Routing

Create a file named server.js. We will import the built-in http module and set up the foundation of our router.

const http = require('http');

// Our temporary in-memory database
const notes = [];

const server = http.createServer((req, res) => {
    // Standardize the response headers to return JSON
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');

    // Extract the URL and Method from the raw request
    const { url, method } = req;

    // --- ROUTING LOGIC ---
    if (url === '/notes') {
        if (method === 'GET') {
            // Handle GET /notes
            res.statusCode = 200;
            res.end(JSON.stringify({ status: 'success', data: notes }));
            return;
        }

        if (method === 'POST') {
            // Handle POST /notes (We will implement this next)
            handlePostRequest(req, res);
            return;
        }
    }

    // Fallback for 404 Not Found
    res.statusCode = 404;
    res.end(JSON.stringify({ status: 'fail', message: 'Route not found' }));
});

const PORT = 5000;
server.listen(PORT, () => {
    console.log(`Vanilla Node.js server running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Step 2: The Tricky Part - Handling Data Streams

This is where framework developers usually get lost. When a client sends a POST request with JSON data, Node.js doesn't just hand you a neatly parsed JavaScript object. It streams the data in binary chunks.

We have to listen for the dataevent, collect the chunks into an array, and then combine them when the endevent fires.

Let's implement the handlePostRequest function above our server block:

const { randomUUID } = require('crypto'); // Built-in Node.js module

function handlePostRequest(req, res) {
    let body = [];

    // 1. Collect data chunks as they arrive
    req.on('data', (chunk) => {
        body.push(chunk);
    });

    // 2. When the stream is finished, process the data
    req.on('end', () => {
        try {
            // Convert Buffer array to string, then parse to JSON
            const parsedBody = JSON.parse(Buffer.concat(body).toString());

            const newNote = {
                id: randomUUID(),
                title: parsedBody.title,
                body: parsedBody.body,
                createdAt: new Date().toISOString()
            };

            notes.push(newNote);

            res.statusCode = 201; // Created
            res.end(JSON.stringify({
                status: 'success',
                message: 'Note added successfully',
                data: { noteId: newNote.id }
            }));
        } catch (error) {
            // Handle bad JSON format
            res.statusCode = 400;
            res.end(JSON.stringify({ status: 'error', message: 'Invalid JSON body' }));
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Testing the Raw API

Because we have absolutely zero external dependencies, you don't even need npm install. Just run:

node server.js
Enter fullscreen mode Exit fullscreen mode

You can test this using cURLor Postman. To add a note, send a POST request:

curl -X POST http://localhost:5000/notes \
-H "Content-Type: application/json" \
-d '{"title": "My First Note", "body": "Understanding vanilla Node.js is awesome."}'
Enter fullscreen mode Exit fullscreen mode

And you will receive a clean, framework-less JSON response.

Architectural Verdict

Will I ever write an enterprise-level API in production using strictly Vanilla Node.js? Absolutely not. Writing manual stream handlers, dealing with complex nested routing paths (like /notes/:id/comments), and managing security headers manually is a waste of engineering time when tools like Express or Hapi exist.

However, building this raw server is a mandatory rite of passage. Once you understand how to manually parse a Buffer stream and construct an HTTP response header, you stop seeing frameworks as magic. You see them for what they truly are: utility wrappers around the http module. This fundamental knowledge is what separates framework operators from true software engineers.


What are your thoughts?
Have you ever tried building a backend without relying on external NPM packages? How deep do you think developers need to understand the underlying modules before jumping into modern frameworks? Let me know in the comments!

Top comments (0)