Web Development

Build and Debug a Small REST API with Node.js

Build a dependency-free REST API with Node.js, test it from the terminal, and use a repeatable debugging workflow when requests fail.

Abdullah IslamAbdullah Islam·
Illustration of an HTTP request and response cycle

A small API is one of the best ways to understand HTTP. Instead of hiding the request and response cycle behind a framework, we will use Node.js’s built-in node:http module, send requests from the terminal, and inspect each result.

By the end, you will have two endpoints:

  • GET /health checks whether the server is running.
  • GET /api/posts/:id returns one post or a 404 response.

The request and response cycle

An HTTP interaction starts when a client sends a method and URL to a server. The server matches that request to a route, performs some work, and sends back a status code, headers, and an optional body.

A client sending an HTTP request to a server and receiving a response
Every API endpoint follows the same basic request and response cycle.
n HTTP interaction starts when a client sends a method and URL to a server. The server matches that request to a route, performs some work, and sends back a status code, headers, and an optional body.

n HTTP interaction starts when a client sends a method and URL to a server. The server matches that request to a route, performs some work, and sends back a status code, headers, and an optional body.

Make one request, observe the complete response, change one thing, and repeat.

The debugging loop used in this guide

Build the API

  1. Create the project

    Open a terminal and create an empty project directory:

    mkdir tiny-node-api
    cd tiny-node-api
    npm init -y

    Open the directory in your editor. In Visual Studio Code, you can use code ..

  2. Create the HTTP server

    Create a file named server.mjs and add the following code:

    import { createServer } from 'node:http';
    
    const posts = new Map([
      ['1', { id: 1, title: 'Understanding HTTP' }],
      ['2', { id: 2, title: 'Debugging Node.js' }],
    ]);
    
    function sendJson(response, status, data) {
      response.writeHead(status, {
        'content-type': 'application/json; charset=utf-8',
      });
      response.end(JSON.stringify(data));
    }
    
    const server = createServer((request, response) => {
      const url = new URL(request.url, 'http://localhost');
    
      console.log(`${request.method} ${url.pathname}`);
    
      if (request.method === 'GET' && url.pathname === '/health') {
        return sendJson(response, 200, { status: 'ok' });
      }
    
      const match = url.pathname.match(/^\/api\/posts\/(\d+)$/);
      if (request.method === 'GET' && match) {
        const post = posts.get(match[1]);
    
        return post
          ? sendJson(response, 200, post)
          : sendJson(response, 404, { error: 'Post not found' });
      }
    
      return sendJson(response, 404, { error: 'Route not found' });
    });
    
    server.listen(3000, '127.0.0.1', () => {
      console.log('API listening on http://127.0.0.1:3000');
    });
  3. Start the server

    Run the file directly with Node.js:

    node server.mjs

    Keep this terminal open. When you are finished, press Ctrl + C to stop the server.

  4. Send your first requests

    Open a second terminal and request the health endpoint:

    curl -i http://127.0.0.1:3000/health

    Then request an existing post and one that does not exist:

    curl -i http://127.0.0.1:3000/api/posts/1
    curl -i http://127.0.0.1:3000/api/posts/99

Read the complete response

The -i option tells curl to include response headers. A successful request should look similar to this:

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8

{"id":1,"title":"Understanding HTTP"}

Read the response in this order:

  1. Status code — did the server report success or failure?
  2. Content type — is the body encoded in the format you expected?
  3. Body — does the returned data match the endpoint’s contract?
  4. Server log — did the method and path reach the application correctly?

This order prevents a common mistake: staring at application code before confirming what actually traveled over the network.

Debug a failing request

Suppose /api/post/1 returns 404. Compare the requested path with the route expression:

Requested: /api/post/1
Expected:  /api/posts/1
                       ^

The server is healthy; the client used the wrong URL. The request log makes that visible without adding a debugger or installing another tool.

Why does a request sometimes keep loading forever?

Every route must eventually call response.end(). In this example, sendJson() ends every response. A branch that writes headers but never ends the response leaves the connection open, so the client continues waiting.

Why does curl report connection refused?

Usually the server is not running, exited because of an error, or is listening on a different port. Check the server terminal first, then verify the host and port in the URL.

Improve the API safely

Once both routes work, extend the server one behavior at a time:

  • Add GET /api/posts to return every post.
  • Reject unsupported methods with 405 Method Not Allowed.
  • Validate data before creating or updating a resource.
  • Add a request ID so one request can be followed across multiple log messages.
  • Write automated tests for successful and unsuccessful responses.

The built-in server is excellent for learning how HTTP works. For a production project, you may eventually choose a framework, but the same request, routing, response, and debugging principles still apply.

Further readingNode.js HTTP documentation (external link)Explore the complete API for creating HTTP servers and handling requests and responses.

A reusable debugging checklist

When an API request fails, work through these questions:

  1. Is the server process running?
  2. Am I using the correct host, port, method, and path?
  3. What status code and body did the server return?
  4. What did the server log for the same request?
  5. Can I reproduce the problem with the smallest possible request?

That process scales from this tiny server to much larger systems. The tools change, but careful observation remains the fastest route to the cause.

Newsletter

Learn with me

Join the newsletter to receive my latest articles. No spam, unsubscribe anytime.

You'll get a confirmation email. Check your inbox to complete your subscription.

Share this post

About the author

Abdullah Islam

Abdullah Islam

Abdullah Islam is a computer networking learner, tech blogger, and enthusiast who shares simple, practical guides to help others understand networking concepts and build real-world skills.

Comments

Similar posts

More articles on related topics.