A minimal HTTP server written in Go from first principles.
This project does not use Go's high-level net/http server. Instead, it implements the core ideas manually:
- open a TCP listener
- accept incoming connections
- read raw HTTP request bytes
- parse the request line, headers, and body
- match the request to a route
- build and send a raw HTTP/1.1 response
The goal of this repository is educational: to understand how an HTTP server works at the byte/string level before relying on higher-level abstractions.
- What this project is
- Why this exists
- How it works
- Project structure
- Request lifecycle
- Implemented routes
- How to run
- How to test with curl
- Core concepts used
- Current limitations
- Known issues in the current implementation
- Possible next improvements
This is a small custom HTTP server built directly on top of TCP.
At a basic level:
- A client connects to port
8080 - The server reads the request as plain text
- The request is parsed into a
Requeststruct - A route handler is selected using the HTTP method and path
- The handler returns a status code and body
- The server formats a raw HTTP response string
- The response is written back to the client
- The connection is closed
This project currently supports:
GETrequestsPOSTrequests- simple route registration
- request header parsing
- request body parsing using
Content-Length - concurrent connection handling with goroutines
Most HTTP servers hide the low-level details. That is convenient, but it also makes the protocol feel magical.
This project removes that magic.
By building the server manually, the repository helps explain:
- what an HTTP request actually looks like
- how headers and body are separated
- how routing works
- how status codes and response headers are constructed
- why TCP matters underneath HTTP
If the objective is learning rather than production use, this approach is useful.
The program listens on port 8080.
listener, err := net.Listen("tcp", ":8080")This opens a socket and waits for incoming TCP connections.
The server runs an infinite loop:
- wait for a client
- accept the connection
- handle that connection in a goroutine
That means multiple clients can be served concurrently.
When a client connects, the server reads bytes from the socket into a buffer.
The current implementation uses a fixed-size buffer of 4096 bytes.
The raw request text is split into:
- headers section
- body section
The request line is expected to look like:
GET /hello HTTP/1.1
From that line, the parser extracts:
- method
- path
- protocol
The remaining header lines are parsed into a map[string]string.
A route key is built as:
METHOD + " " + PATH
Examples:
GET /GET /aboutPOST /data
That key is used to look up a handler function in a global route table.
Each route handler receives a Request value and returns:
- an HTTP status code
- a response body
The server manually formats a response that looks like this:
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 25
Connection: close
Hello, world!The response string is written to the TCP connection, and the connection is closed.
HTTP-GO/
├── main.go
├── README.md
└── .github/
└── instructions/
└── first-principles.instructions.md
Contains the full server implementation:
Requeststruct- route registration
- request parsing
- connection handling
- response construction
- server startup
Repository guidance that asks explanations and generated content to start from fundamentals and build upward.
This is the current end-to-end flow:
main()registers routesmain()starts a TCP listener on:8080- A client sends an HTTP request
handleRequest(conn)reads the raw bytesparseRequest(rawData)converts the text into aRequest- The server looks up a matching route
- If found, the handler runs
- If not found, the server returns
404 - The response string is written to the socket
- The connection closes
Returns:
Welcome to the homepage!
Returns:
This is the about page!
Returns:
Hello, world!
Echoes the request body back to the client:
You sent: <body>
Example:
- Request body:
test - Response body:
You sent: test
- Go installed
- Linux, macOS, or Windows terminal
- a browser or
curl
From the project root:
go run main.goExpected output:
TCP server listening on port 8080...
Waiting for connections....
Visit:
http://localhost:8080/http://localhost:8080/abouthttp://localhost:8080/hello
curl -i http://localhost:8080/curl -i http://localhost:8080/aboutcurl -i http://localhost:8080/hellocurl -i -X POST http://localhost:8080/data -d "sample body"curl -i http://localhost:8080/missingExpected status:
404 Not Found
HTTP is an application-layer protocol that usually runs on top of TCP.
TCP provides:
- connection establishment
- ordered delivery
- reliable byte streams
This server uses TCP directly, then implements a small piece of HTTP on top of it.
An HTTP request is plain text with a specific structure:
POST /data HTTP/1.1
Host: localhost:8080
Content-Length: 11
hello worldImportant parts:
- request line: method, path, protocol
- headers
- blank line
- body
Routing means selecting behavior based on:
- HTTP method
- request path
This project uses a map key like:
GET /hello
A handler is a function that takes a parsed request and produces a response.
This project defines handlers with the shape:
func(req Request) (statusCode int, body string)This is a learning project, not a production-ready server.
Current limitations include:
- only a few hardcoded routes
- only plain text responses
- no middleware
- no query string parsing
- no dynamic path parameters
- no JSON support
- no chunked transfer encoding
- no support for large or streamed request bodies
- no keep-alive connection handling
- no HTTPS/TLS
- no robust error response formatting
- no request validation beyond basic parsing
These are important if the repository is meant to teach accurate HTTP behavior.
The response string currently builds:
Content-Length: %dConnection: close
There should be a line break between those headers.
Without the missing \r\n, some clients may parse the response incorrectly.
The server reads only once into a 4096 byte buffer.
That means:
- large headers may be truncated
- large bodies may be truncated
- some requests may arrive in multiple TCP reads but only the first read is processed
If request parsing fails, the server logs the error and returns without sending a proper HTTP error response to the client.
The parser trims the body if it is longer than Content-Length, but it does not ensure the full body was actually read from the socket.
Only a small set of status texts is handled manually:
200 -> OK400 -> Bad Request404 -> Not found
This is enough for a demo but incomplete.
Routes are stored in a global variable. That is acceptable in this small example, but larger systems usually encapsulate router state.
The current log prints:
- method
- request body
It does not consistently log path, headers, timing, or client address.
If extending this project, useful next steps would be:
- fix response header formatting
- read from the socket until the full request is available
- return proper
400 Bad Requestresponses on parse failures - separate router, parser, and response writer into different files
- add unit tests for request parsing and routing
- support query parameters
- support JSON request and response bodies
- add a helper for status text lookup
- normalize response generation into reusable functions
- add graceful shutdown support
This repository demonstrates that an HTTP server is fundamentally:
- a TCP listener
- a request parser
- a router
- a response formatter
Frameworks automate these pieces, but the pieces themselves are not mysterious. This project exposes them directly so they can be understood and rebuilt from basic principles.
No license file is currently present in the repository.
If this project is intended for sharing or reuse, add a LICENSE file explicitly.