TeachyLeaf library

What are you looking for?

Search practical tutorials and browser-based tools.

Node.js · HTTP · Beginner

Build a small HTTP server with Node.js

Create two GET routes with Node’s built-in HTTP module, read a query parameter, return JSON, and check the response status.

Who this is for: Beginners who know basic JavaScript and want to see how a Node.js process can receive and answer HTTP requests.

What you will learn: Start a server with node:http, match request methods and paths, parse a query string with URL, and return JSON with deliberate status codes.

Prerequisites: Basic JavaScript functions, objects, and strings, plus Node.js installed on your computer.

Tested environment: Node.js v24.22.1 using only built-in modules. The code uses an .mjs file so it does not depend on a package manifest or a third-party framework.

1. Create a small server

Save this complete example as server.mjs. It exposes a health route and a greeting route. Both accept only GET; an unknown path returns 404, while a known path with another method returns 405.

import { createServer } from "node:http";

const hostname = "127.0.0.1";
const port = 3000;

function sendJson(response, statusCode, data) {
    response.writeHead(statusCode, {
        "Content-Type": "application/json; charset=utf-8",
    });
    response.end(JSON.stringify(data));
}

const server = createServer((request, response) => {
    let requestUrl;

    try {
        requestUrl = new URL(request.url ?? "/", `http://${hostname}:${port}`);
    } catch {
        sendJson(response, 400, { error: "Invalid request URL" });
        return;
    }

    const knownPath =
        requestUrl.pathname === "/health" || requestUrl.pathname === "/greet";

    if (!knownPath) {
        sendJson(response, 404, { error: "Route not found" });
        return;
    }

    if (request.method !== "GET") {
        response.writeHead(405, {
            "Allow": "GET",
            "Content-Type": "application/json; charset=utf-8",
        });
        response.end(JSON.stringify({ error: "Method not allowed" }));
        return;
    }

    if (requestUrl.pathname === "/health") {
        sendJson(response, 200, { status: "ok" });
        return;
    }

    const requestedName = (requestUrl.searchParams.get("name") ?? "").trim();

    if ([...requestedName].length > 80) {
        sendJson(response, 400, { error: "Name must be 80 characters or fewer" });
        return;
    }

    const name = requestedName || "friend";
    sendJson(response, 200, { message: `Hello, ${name}!` });
});

server.listen(port, hostname, () => {
    console.log(`Server listening at http://${hostname}:${port}`);
});

2. Run it and inspect the responses

In a terminal, start the server:

node server.mjs

Leave it running, then use a second terminal to make requests. The -i option shows the HTTP status and headers as well as the response body. In PowerShell, use curl.exe to avoid the legacy curl alias; on macOS or Linux, use curl:

curl.exe -i http://127.0.0.1:3000/health
curl.exe -i "http://127.0.0.1:3000/greet?name=Sam"
curl.exe -i http://127.0.0.1:3000/missing
curl.exe -i -X POST http://127.0.0.1:3000/greet

The health request returns 200 and {"status":"ok"}. The greeting returns 200 and {"message":"Hello, Sam!"}. The missing route returns 404; POST to a known route returns 405 with Allow: GET.

Try /greet?name=Alex%20Kim to see URL decoding, or omit the name to get the default greeting. More than 80 Unicode code points is rejected with 400 rather than silently truncated.

3. Follow the request through the code

  1. createServer registers a callback that runs for each incoming request.
  2. new URL(...) separates the path and query parameters. The base URL is fixed; the server does not trust a client-supplied host header to build it.
  3. The path and method checks choose a response. A method mismatch on a known route is different from a path that does not exist.
  4. searchParams.get("name") reads and percent-decodes the query value. The code trims it, applies an explicit size limit, and falls back to “friend” when it is blank.
  5. JSON.stringify serializes the data, while the response header identifies the body as JSON with UTF-8 text.

Common debugging checks

  • EADDRINUSE: another process is already listening on port 3000. Stop that process or choose another port in the example and request URLs.
  • Connection refused: check that node server.mjs is still running and that the request uses 127.0.0.1 and the same port.
  • Unexpected 404: compare the path exactly. This example matches /greet, not /greet/, and it does not define a root route.
  • Unexpected greeting: check the query key is name. A URL such as ?user=Sam does not set it.
  • Stop the server: press Ctrl+C in the terminal running it.

Limits and production considerations

This is a learning example, not a production-ready web service. It binds only to the loopback interface on your own computer; it has no TLS, authentication, request-body handling, logging policy, rate limiting, or graceful-shutdown logic. Do not expose it directly to the public internet.

The response is JSON, not HTML. If you later render user-supplied values into HTML, use context-appropriate escaping rather than assuming JSON serialization makes HTML safe. A real application should also define operational limits and validate every input according to its purpose.

References

Continue learning

Continue with reading and validating a JSON request body, compare the flow with the Laravel request lifecycle, or browse the programming tutorials and tools.