Go back

i built an http server from scratch

July 17, 2025

http is a set of rules that computers use to communicate over the internet. when a client wants something, like a webpage, it sends an http request to the server and the server processes the request, finds the resource, and sends an http response back over tcp.

it all boils down to two things: http requests and http responses.

these are the messages sent back and forth and formatted in a specific way so both sides understand each other.

an http request has three main parts:

  • request line: says what the client wants. it includes the method, the path, and the http version.

  • headers: extra info, like the browser type or what formats the client accepts.

  • body (optional): data sent with the request.

example of a valid http request:

GET /echo/hello HTTP/1.1
Host: localhost:4221
User-Agent: curl/7.81.0
Accept: text/plain

this is a GET request asking for the /echo/hello path. the headers say it’s coming from localhost and curl. there’s no body here since it’s a GET request.

server reads this and decide what to send back.

an http response is what the server sends back to answer the client’s request. it also has three parts:

  • status line: tells the client if the request worked, with a status code (like 200 for success or 404 for not found) and a short description.

  • headers: extra info, like the type of content or its size.

  • body (optional): the actual content, like a webpage’s text or a file’s data.

example of an http response:

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5

hello

this response says the request was successful, the content is plain text, and it’s 5 characters long. the body contains the word hello.

when i set out to build the http server, i needed to figure out the bare minimum to make it work.

i decided to create an HttpHelper class because parsing http requests and crafting responses felt like repetitive tasks that could get messy fast.

every request needed to be broken down into parts like the method or path, and every response needed proper formatting with status codes and headers.

the HttpHelper class handles two main jobs:

  1. it takes the raw data from a client and splits it into useful pieces that then i can work with, like the url path or headers.

  2. it creates properly formatted responses with status codes, headers and content.

for example when a browser sends a request like GET /echo/hello, HttpHelper figures out the path and lets my server decide what to send back.

then it packages the response (like 200 OK with the word hello) in a way the browser understands.

the goal is to create a server that could handle basic web requests, like fetching a webpage or saving a file without relying on third party frameworks.

here’s the key requirements:

listening for connections

the http server needs to listen for incoming client requests over the internet. since http runs on tcp, i used nodejs’s net module to create a tcp server that listens on a port, mine runs on 4221:

const server = net.createServer((socket) => {
  // handle incoming connections
});

server.listen(4221, "localhost");

now i have a server that listens for connections on localhost:4221. each connection creates a socket, which i use to process requests.

parsing http requests

clients send http requests with details like the method, url path, headers and sometimes a body. i needed to parse these requests to understand what the client wanted.

parseBuffer(str: string): Request {
  const [_, body] = str.split(CRLF + CRLF);
  const [reqLine, ...rest] = _.split(CRLF);

  const [method, pathname, protocol] = reqLine.split(" ");

  const headers = new Map<string, string>();

  for (const line of rest) {
    const index = line.indexOf(":");

    if (index !== -1) {
      const key = line.slice(0, index).trim();
      const value = line.slice(index + 1).trim();
      headers.set(key, value);
    } else {
      headers.set("Request-Line", line);
    }
  }

  return {
    method,
    path: pathname.split("/").filter(Boolean),
    protocol,
    headers: headers,
    body,
  };
}

my parseBuffer method splits the raw request string into its parts (method, path, headers, body). For example, a request like GET /echo/hello HTTP/1.1 gets broken down:

{
  "method": "GET",
  "path": ["echo", "hello"],
  "protocol": "HTTP/1.1",
  "headers": {
    "Host": "localhost:4221",
    "User-Agent": "curl/7.81.0",
    "Accept": "text/plain"
  },
  "body": null
}

so I can process the /echo/hello path.

sending http responses

once i parsed the request, i needed to send back a proper http response with a status code, headers, and a body.

generateResponse(options: ResponseOptions): {
  headers: string;
  body: Uint8Array | string;
} {
  const { status, contentType, contentLength, body = "", headers } = options;
  let _headers = `HTTP/1.1 ${status} ${statusMessages[status]}${CRLF}`;
  let _body: Uint8Array | string = body;
  let shouldCompress = false;

  if (contentType) _headers += `Content-Type: ${contentType}${CRLF}`;

  if (headers) {
    Array.from(headers, ([header, value]) => ({ header, value })).forEach(
      (item) => {
        switch (item.header) {
          case "Accept-Encoding": {
            const encodings = item.value
              .split(",")
              .filter((i) => supportedEncodings.includes(i.trim()))
              .join(", ")
              .trim();

            if (encodings.length) {
              shouldCompress = true;

              _headers += `Content-Encoding: ${encodings}${CRLF}`;
            }
            break;
          }
          default: {
            _headers += `${item.header}: ${item.value}${CRLF}`;
            break;
          }
        }
      },
    );
  }

  if (shouldCompress) {
    _body = new Uint8Array(gzipSync(new TextEncoder().encode(body)));
    _headers += `Content-Length: ${_body.length}${CRLF}`;
  } else if (contentLength !== undefined)
    _headers += `Content-Length: ${_body.length}${CRLF}`;

  _headers += CRLF;

  return {
    headers: _headers,
    body: _body,
  };
}

my generateResponse function builds a response with the right status and headers. for example, a 200 OK response with text content might look like:

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5

hello.

handling routes

the server needs to respond differently based on the url path, like / for a homepage or /echo to echo back data. i used a simple switch statement to route requests to the right logic.

const base = req.path[0];
const param = req.path[1];
switch (base) {
  case undefined: {
    httpHelper.socket.write({ status: 200, headers: req.headers });
    break;
  }
  case "echo": {
    httpHelper.socket.write({
      status: 200,
      contentType: "text/plain",
      contentLength: param.length,
      body: param,
    });
    break;
  }
}

i checked the first part of the path like echo in /echo/abc and sent back the appropriate response, like echoing the string abc for /echo/abc.

supporting file operations

i wanted my server to handle file requests, like serving a file’s contents for GET /files/filename or saving data for POST.

I used nodejs’s fs module to read files for GET requests, checking if the file exists and sending a 404 if it doesn’t.


this is basically it! it’s not a fancy server but it handle real requests. if you want to play with it yourself, you can use some curl commands to test what it can do.

try these curl commands (assuming the server is running on localhost:4221 and a directory like /tmp is set with --directory):

root path

this hits the / path, and the server sends a simple 200 OK response with no body, just confirming it’s alive.

curl -v http://localhost:4221/

Response:

HTTP/1.1 200 OK

echo

The server echoes back “hello” from the /echo/hello path as plain text.

curl -v http://localhost:4221/echo/hello

Response:

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5

hello

user agent

this returns the User-Agent header you sent.

curl -v -A "curl/7.81.0" http://localhost:4221/user-agent

Response:

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11

curl/7.81.0

file request (GET):

The server serves the file’s contents, or a 404 if the file doesn’t exist.

curl -v http://localhost:4221/files/test.txt

Response (assuming test.txt exists in /tmp):

HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: <file-size>

<file-content>

if you want the server to close the connection after a response, send a request with a Connection: close header.