A fully custom HTTP server implemented in Go, supporting request parsing, partial parsing, static file serving, and dynamic endpoint handling.
We’re creating a fully customized HTTP server in Go — no external libraries, just pure Go. You might be wondering, why reinvent the wheel when net/http
already exists? Well, if you’re a nerd like me and love understanding how things work under the hood, this is the perfect blog for you. For example, you’ll see how to handle partial parsing of requests.
All you need before diving in:
/users/{id}
.This isn't the best file structure. feel free to adjust it to your needs. Here's the current layout.
cmd/main.go
internals/http/
common.go
→ Shared HTTP functions and constants.headers.go
→ Functions and types for handling HTTP headers.request.go
→ Parsing and handling HTTP requests.response.go
→ Constructing and sending HTTP responses.internals/server/
middleware.go
→ Middleware logic (logging, authentication, etc.).route.go
→ Route definitions and parameter parsing.server.go
→ Main server setup, listener, and request handling.internals/type/
contentType.go
→ MIME types.error.go
→ Custom error types.Method.go
→ HTTP methods (GET, POST, etc.).parseState.go
→ Internal parsing states for request handling.statusCode.go
→ HTTP status codes (200, 404, 500, etc.).version.go
→ HTTP versions (1.0, 1.1, etc.).internals/utils/url.go
static/
1my-server/
2├─ cmd/
3│ └─ main.go
4├─ internals/
5│ ├─ http/
6│ │ ├─ common.go
7│ │ ├─ headers.go
8│ │ ├─ request.go
9│ │ └─ response.go
10│ ├─ server/
11│ │ ├─ middleware.go
12│ │ ├─ route.go
13│ │ └─ server.go
14│ ├─ type/
15│ │ ├─ contentType.go
16│ │ ├─ error.go
17│ │ ├─ Method.go
18│ │ ├─ parseState.go
19│ │ ├─ statusCode.go
20│ │ └─ version.go
21│ └─ utils/
22│ └─ url.go
23└─ static/
24
25
Let’s start with parsing the request. This can be the trickiest part at least it was for me. You might be wondering why. The thing is, in TCP, you might receive only part of the request in the first packet. For example, you could get just the method and path, then the next packet might contain headers, and maybe only part of the body.
The good news is, we don’t need to worry about putting everything in order TCP already handles that for us using sequence numbers. That itself is an interesting topic for another time.
For example, the first packet might contain:
GET /API
Looks incomplete, right? Then the next packet could bring:
HTTP/1.0\r\n
Once we combine them, we start to get the full request. From there, we can continue parsing headers, body, and everything else we need. full request will be GET /API HTTP1.0\r\n
with all in mind let's start doing this.
RequestLine
holds the HTTP types.Method
just method enum, path, and version. Request
combines the request line, headers, body, parsing state like are we currently parsing Header or body or what this will make since in a mini, and URL parameters into one structure. It represents the full HTTP request received by the server.
1type RequestLine struct {
2 Method types.Method
3 Path string
4 Version types.Version
5}
6type Request struct {
7 RequestLine RequestLine
8 Body []byte
9 Headers Header
10 status types.ParseState
11 Params url.Params
12}
13func NewRequestParser() *Request {
14 return &Request{
15 status: types.StateRequestLine,
16 Body: nil,
17 Headers: make(Header),
18 }
19}
Don’t get scared, this function is actually pretty simple. First, we create a buffer of fixed size (like 1024 bytes) and initialize our request structure. We also set up buffLen
, which keeps track of how much of the buffer is already filled.
Then we enter a loop that keeps reading from the client until the request is fully parsed (req.status == StateDone
).
bytesRead, err := reader.Read(buff[buffLen:])
.This line reads data from the client and writes it into the buffer starting from buffLen
. After reading, we update buffLen
so we know how much data is in the buffer.
Next, we call the parser:
consumed, err := req.parsing(buff[:buffLen])
.It will try to parse as much as it can. consumed tells us how many bytes were used. If we didn’t finish parsing or got an EOF, we handle that.
Finally, any leftover data in the buffer (that wasn’t consumed) is moved to the front so we can reuse the same buffer for the next read:
copy(buff, buff[consumed:buffLen])
buffLen -= consumed
magine your buffer has "GET / HTTP/1.1\r\nHost: loca"
and you just parsed "GET / HTTP/1.1\r\n"
(16 bytes). That leaves "Host: loca"
unprocessed.
By moving the leftover to the front and reducing the buffer length, the next read will append new data right after "Host: loca"
. This way, you don’t lose any unprocessed bytes and can keep using the same buffer efficiently.
We repeat this until the request is done. At the end, we return the fully parsed request. read → parse → move leftovers → repeat until done.
1func ParseRequest(reader io.Reader) (*Request, error) {
2 buff := make([]byte, DefaultBufferSize)
3 req := NewRequestParser()
4 buffLen := 0
5 for req.status != types.StateDone {
6
7 bytesRead, err := reader.Read(buff[buffLen:])
8 if err != nil {
9 return nil, err
10 }
11 buffLen += bytesRead
12
13 consumed, err := req.parsing(buff[:buffLen])
14 if err != nil {
15 // if the error EOF. change the status to Done
16 if errors.Is(err, io.EOF) {
17 req.status = types.StateDone
18 break
19 }
20 return nil, err
21 }
22
23 // Move leftover data (buff[consumed:buffLen]) to the front (index 0)
24 // so we can reuse the buffer for the next read.
25 copy(buff, buff[consumed:buffLen])
26 buffLen -= consumed
27 }
28
29 return req, nil
30
31}
32
Its job is to read the request step by step: first the request line, then headers, and finally the body. We also keep track of how many bytes we’ve “used” from the buffer with consumed
.
We start a loop that keeps checking the current parsing state:
StateRequestLine
) parseRequestLine
on the current data.consumed
and move to headers.StateHeader
) parseRequestHeader
.consumed
if we parsed something, then move to body.StateBody
) Content-Length
to know how many bytes to read.req.Body
.The key idea: read a bit → parse a bit → keep track of consumed → repeat until done. This way we can handle requests that arrive in multiple packets.
1func (req *Request) parsing(data []byte) (int, error) {
2 consumed := 0
3 for {
4 currentData := data[consumed:]
5 switch req.status {
6 case types.StateRequestLine:
7 reqLen, err := req.parseRequestLine(currentData)
8
9 if err != nil {
10 return consumed, err
11 }
12 if reqLen == 0 {
13 return consumed, nil
14 }
15
16 consumed += reqLen
17 req.status = types.StateHeader
18 case types.StateHeader:
19 headerLen, err := req.parseRequestHeader(currentData)
20 if err != nil {
21 return consumed, err
22 }
23 if headerLen == 0 {
24 return consumed, nil
25 }
26 consumed += headerLen
27 req.status = types.StateBody
28 case types.StateBody:
29 length, err := req.Headers.Get("Content-Length")
30 if err != nil {
31 req.status = types.StateDone
32 }
33 n, err := strconv.Atoi(length)
34 if err != nil || n == 0 {
35 req.status = types.StateDone
36 }
37 readLength := min(n-len(req.Body), len(currentData))
38 if readLength == 0 {
39 // We've read everything available so far, but not the full body
40 // Return control so caller can read more data into buffer
41 return consumed, nil
42 }
43 req.Body = append(req.Body, currentData...)
44 consumed += readLength
45 if len(req.Body) >= n {
46 req.status = types.StateDone
47 }
48 case types.StateDone:
49 return consumed, nil
50 }
51 }
52}
Let’s focus on parsing the request line first. The request line is the very first line of an HTTP request and looks like METHOD PATH VERSION
, ending with \r\n
.
The first thing we do is check if the data we have contains \r\n
. If it doesn’t, that means we haven’t received the full line yet, so we just return 0 and no error. This tells the parser: “keep reading, we’re not done yet.”
Once we find the \r\n
, we know we have the full line, so we can start processing it. We split the line by spaces to get three parts: the method, the path, and the version. If there aren’t exactly three parts, it’s an invalid request line and we return an error.
Next, we parse the method and version using helper functions, and save the method, path, and version into the request structure. Finally, we return the number of bytes we consumed (the length of the line plus the \r\n
) so the parser knows how much of the buffer was used.
1
2func (req *Request) parseRequestLine(data []byte) (int, error) {
3 // method path version \r\n
4 idx := bytes.Index(data, []byte(SEPARATOR))
5 // we did not get the full line
6 // so do not return error
7 if idx == -1 {
8 return 0, nil
9 }
10 parts := bytes.Split(data[:idx], []byte(" "))
11 if len(parts) != 3 {
12 return 0, fmt.Errorf("%w: expected 3 parts, got %d", ErrInvalidRequestLine, len(parts))
13 }
14 method, err := types.ParseMethod(parts[0])
15 if err != nil {
16 return 0, err
17 }
18 version, err := types.ParseVersion(parts[2])
19 if err != nil {
20 return 0, err
21 }
22 req.RequestLine.Method = method
23 req.RequestLine.Version = version
24 req.RequestLine.Path = string(parts[1])
25 return idx + len(SEPARATOR), nil
26}
With this mechanism, we can receive requests partially. Let’s test it by sending characters one by one to see if our server handles it. Real TCP packets aren’t exactly like this, but it’s good for testing.
1 cat sending.sh
2#!/bin/bash
3
4text=$'GET / HTTP/1.1\r\nHost: localhost:8080\r\nConnection: close\r\n\r\n'
5
6for ((i=0; i<${#text}; i++)); do
7 printf "%s" "${text:$i:1}"
8 sleep 0.1
9done | nc localhost 8080
Header parsing is simple. Each header line looks like key: value\r\n
. We can store them in a map so it’s easy to look up values by key:
type Header map[string]string
This way, Content-Length
, Host
, or any header can be accessed quickly. so we can create some helper to help us like
func (h *Header) Delete(key string) error
func (h Header) ForEach(f func(key, value string))
func (h *Header) Get(key string) (string, error)
func (h *Header) Set(key, value string) error
the response structure slimier to the request but with some differences. the only different as we can see the status line the Header and body structure are the exact same.
ResponseWriter
is the structure we use to build and send HTTP responses.
write
: where the response bytes go (usually the client connection).idleTimeout
: how long to wait before closing the connection if nothing happens. here i'm using it for keep alive it can be configured.isKeepAlive
: tells if the connection should stay open after sending the response.Version
: HTTP version for this response (like 1.0 or 1.1). this just enum values in the types folderStatus
: the HTTP status code (200, 404, etc.). enum values in the types.Headers
: pointer to a map of headers (key: value
) to send with the response.Basically, this struct holds everything needed to construct and send a proper HTTP response to the client.
1type ResponseWriter struct {
2 write io.Writer
3 idleTimeout time.Duration
4 isKeepAlive bool
5 Version types.Version
6 Status types.StatusCode
7 Headers *Header
8}
9func NewResponseWriter(w io.Writer, idleTimeout time.Duration) *ResponseWriter {
10 return &ResponseWriter{
11 write: w,
12 idleTimeout: idleTimeout,
13 isKeepAlive: false,
14 Version: types.HTTP1_1,
15 Status: types.OK,
16 Headers: NewHeader(),
17 }
18}
19
I split sending the status line, headers, and body into separate functions so anyone creating an endpoint can easily customize the response, like changing headers, status, or body, without touching the core logic. You can set headers manually, or use the helper function I added to handle everything automatically, like func (w *ResponseWriter) SendResponse(body []byte) *types.RouteError
, which takes the body and sets headers and sends the response dynamically.
let's start by sending the status line,function sends the status line of the HTTP response, which is the first line like HTTP/1.1 200 OK
.
ResponseWriter
(w.Status
).OK
for 200) from types.StatusText
.\r\n
.1func (w *ResponseWriter) WriteStatusLine() error {
2 code := w.Status
3 text, ok := types.StatusText[code]
4 if !ok {
5 return ErrUnknownStatusCode
6 }
7 _, err := fmt.Fprintf(w.write, "%s %d %s\r\n", w.Version.String(), code, text)
8 return err
9}
10
The WriteHeader
function sends all the HTTP headers to the client. It goes through each key-value pair in the Headers
map and writes them as Key: Value\r\n
. After sending all headers, it adds an extra \r\n
to signal the end of the header section. This tells the browser or client things like content type, content length, or connection info.
1func (w *ResponseWriter) WriteHeader() error {
2 for key, value := range *w.Headers {
3 if _, err := fmt.Fprintf(w.write, "%s: %s\r\n", key, value); err != nil {
4 return err
5 }
6 }
7 _, err := fmt.Fprint(w.write, "\r\n")
8 return err
9}
10
The WriteBody
function is the simplest part: it just sends the actual response body to the client. Whatever bytes you pass to it HTML, JSON, text, or even an image get written directly to the connection.
1
2func (w *ResponseWriter) WriteBody(data []byte) error {
3 _, err := w.write.Write(data)
4 return err
5}
With all of that, let’s create two more functions to make sending responses easier. The first function is SetDefaultHeaders
. Every time we send a response, we would otherwise need to manually set headers like Content-Length
, Content-Type
, Date
, and possibly others. Doing this repeatedly would be tedious, so this function handles it automatically.
Here’s what it does:
1func (w *ResponseWriter) SetDefaultHeaders(body *[]byte) {
2 // Set Content-Length if not already set
3 if _, exists := (*w.Headers)["Content-Length"]; !exists {
4 w.Headers.Set("Content-Length", strconv.Itoa(len(*body)))
5 }
6
7 // Set Date if not already set
8 if _, exists := (*w.Headers)["Date"]; !exists {
9 w.Headers.Set("Date", time.Now().UTC().Format(time.RFC1123))
10 }
11
12 // Set Connection / Keep-Alive headers
13 if _, exists := (*w.Headers)["Connection"]; !exists {
14 if w.isKeepAlive {
15 w.Headers.Set("Connection", "keep-alive")
16 w.Headers.Set("Keep-Alive", fmt.Sprintf("timeout=%d", int(w.idleTimeout.Seconds())))
17 } else {
18 w.Headers.Set("Connection", "close")
19 }
20 }
21
22 // Set Content-Type if not already set and body exists
23 if len(*body) > 0 {
24 if _, exists := (*w.Headers)["Content-Type"]; !exists {
25 ct := detectContentType(body)
26 w.Headers.Set("Content-Type", string(ct))
27 }
28 }
29}
30
31
32
The second function, SendResponse
, puts everything together. Here’s how it works:
SetDefaultHeaders
– prepares all required headers dynamically.You might wonder why this function returns *types.RouteError
. This is because it’s meant to be called at the endpoint handler level, so any errors while sending the response can be handled consistently in one place.
1
2func (w *ResponseWriter) SendResponse(body []byte) *types.RouteError {
3 w.SetDefaultHeaders(&body)
4
5 if err := w.WriteStatusLine(); err != nil {
6 return &types.RouteError{Code: types.InternalServerError, Message: err.Error()}
7 }
8 if err := w.WriteHeader(); err != nil {
9 return &types.RouteError{Code: types.InternalServerError, Message: err.Error()}
10 }
11 if len(body) > 0 {
12 if err := w.WriteBody(body); err != nil {
13 return &types.RouteError{Code: types.InternalServerError, Message: err.Error()}
14 }
15 }
16 return nil
17}
There are also other functions like getContentTypeFromExtension
, SendFile
, SendBadRequest
, SendNotFound
, and so on. These are straightforward and mostly self-explanatory. If you want to see exactly how they work, you can check the GitHub repo they handle common tasks like determining content type from a file extension, sending a file, or sending standard error responses.
When we receive a request, we need a way to determine which handler to run. For example, we might have an endpoint like GET /api/info
. This means that when a GET request is sent to this path, a specific function should run.
Another example is POST /edit/user/{id}
. This endpoint uses the POST method to edit a user. The {id}
in brackets indicates a dynamic value, meaning it can be any user ID. We need a way to capture this value so the handler knows which user to edit.
so the Handler that going to handle the request will have the following type, it will take response and request as pointer, and return type error
type Handler func(w *http.ResponseWriter, r *http.Request) *types.RouteError
the type error is like this, if the handler Fails will return this error or
1type RouteError struct {
2 Code StatusCode
3 Message string
4}
5
6func (r *RouteError) Error() string {
7 return r.Message
8}
9
Now we need a type to store these handlers so we can look them up when a request comes in. We organize them first by HTTP method, and under each method, we store the path as the key and the corresponding handler as the value.
type Routes map[types.Method]map[string]Handler
This function registers a new route. It takes an HTTP method, a path, and a handler function. If there’s no map for that method yet, it creates one, then stores the handler under the given path so the server knows which function to run when a matching request comes in.
1func (s *Server) Handle(method types.Method, path string, handler Handler) {
2
3 if s.routes[method] == nil {
4 s.routes[method] = make(map[string]Handler)
5 }
6
7 s.routes[method][path] = handler
8}
This function looks up which handler should run for an incoming request. First, it checks if there are any routes registered for the given HTTP method. Then it cleans and splits the request path into segments.
It compares the request path segments with each registered route’s segments. If a segment in the route is a parameter (like {id}
), it captures the value from the request into a params
map. If all segments match, it returns the corresponding handler.
If no matching route is found, it returns a default “Not Found” handler. If the HTTP method isn’t supported, it returns a “Method Not Allowed” handler.
1func (s *Server) FindRoute(path string, method types.Method) (Handler, url.Params) {
2
3 methodRoutes, ok := s.routes[method]
4 if !ok {
5 return http.MethodNotAllowedHandler, nil
6 }
7
8 url.CleanURL(&path)
9
10 reqSegments := strings.Split(strings.Trim(path, "/"), "/")
11
12 for routePath, handler := range methodRoutes {
13 routeSegments := strings.Split(strings.Trim(routePath, "/"), "/")
14 if len(routeSegments) != len(reqSegments) {
15 continue
16 }
17
18 params := make(url.Params)
19 matched := true
20
21 for i, seg := range reqSegments {
22 rSeg := routeSegments[i]
23 if url.IsParam(rSeg) {
24 paramName := rSeg[1 : len(rSeg)-1]
25 params[paramName] = seg
26 } else if rSeg != seg {
27 matched = false
28 break
29 }
30 }
31
32 if matched && handler != nil {
33 return handler, nil
34 }
35 }
36
37 return http.NotFoundHandler, nil
38}
39
Middleware lets you run code before or after your main handler. You can chain multiple middleware functions that wrap around the final handler, so requests pass through them in order. for know it only run the middleware after the handler it run and it run for every single handler, later we can enhance it to make it run only for cretin group.Middleware
is a function type that takes a Handler
and returns a new Handler
. This lets you wrap extra logic around the main handler, like logging, authentication, or modifying requests/responses.
MiddlewareChain
is a structure that stores multiple middleware functions in order. NewMiddlewareChain
creates an empty chain ready to add middleware. This setup allows applying all middlewares to a handler in sequence.
1type Middleware func(next Handler) Handler
2
3type MiddlewareChain struct {
4middlewares []Middleware
5}
6func NewMiddlewareChain() *MiddlewareChain {
7 return &MiddlewareChain{
8 middlewares: make([]Middleware, 0),
9 }
10}
Use
function This function adds a middleware to the chain.
mc.middlewares
is a slice that stores all middleware functions in the order they were added.append(mc.middlewares, middleware)
adds the new middleware to the end of the slice.Apply
function This function “wraps” the final handler with all the middlewares.
finalHandler
is the actual endpoint handler you want to execute after all middleware logic.handler := finalHandler
starts with the endpoint itself.for
loop goes through each middleware in the chain and wraps the current handler
with it:handler = m(handler)
handler
is a new function where all middlewares are applied in order, and calling it will first run middleware logic, then eventually the final handler.Each middleware can decide to run some code before calling the next handler in the chain, giving you full control over request and response flow.
1func (mc *MiddlewareChain) Use(middleware Middleware) {
2 mc.middlewares = append(mc.middlewares, middleware)
3}
4
5// Apply applies all middlewares to a handler in reverse order
6// M1 → M2 → M3 → FinalHandler
7func (mc *MiddlewareChain) Apply(finalHandler Handler) Handler {
8 handler := finalHandler
9 for _, m := range mc.middlewares {
10 handler = m(handler)
11 }
12 return handler
13}
14
Here we manage clients, listen for incoming connections, handle routing, and trigger middlewares. let's see.
closed
is a boolean that indicates whether the server is currently running or has been shut down.listener
is the network listener that waits for incoming TCP connections on a specified port.idleTimeout
defines how long the server will keep a connection alive when using Keep-Alive before closing it automatically.middlewares
stores a chain of middleware functions that can modify requests or responses before they reach the final route handler. we will see it later.routes
is a map of all the endpoints the server knows about, used to match incoming requests to the correct handler. we will see it later1type Server struct {
2 closed bool
3 listener net.Listener
4 idleTimeout time.Duration
5 middlewares *MiddlewareChain
6 routes Routes
7}
8func NewServer(keepAlive time.Duration) *Server {
9 return &Server{
10 closed: false,
11 idleTimeout: keepAlive,
12 middlewares: NewMiddlewareChain(),
13 routes: make(Routes),
14 }
15}
16
This function starts a new HTTP server on the given port. It first creates a server with a 10-second idle timeout, then opens a TCP listener on that port. If the listener fails, it returns an error. Otherwise, it assigns the listener to the server, starts accepting connections in a separate goroutine, and returns the running server.
1func ServeHTTP(port uint16) (*Server, error) {
2 server := NewServer(10 * time.Second)
3 listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
4 if err != nil {
5 return nil, err
6 }
7 server.listener = listener
8 go server.acceptor()
9 return server, nil
10}
This function handles a single client connection. It’s the core of the server.
Close connection when donedefer conn.Close()
ensures the connection is always closed when the function exits, regardless of errors or normal completion. This prevents resource leaks.
Loop to handle multiple requests
The for
loop allows the server to handle multiple requests on the same connection. This is essential for HTTP Keep-Alive, where the client can send multiple requests without reopening a connection.
Set idle timeout
If the server has an idleTimeout
set, it applies a deadline to the connection. This prevents a client from keeping the connection open indefinitely and helps free resources automatically if the connection is idle for too long.
Parse incoming request
The server reads the raw request from the client and converts it into a structured request object using http.ParseRequest(conn)
. If parsing fails, the server responds with a 400 Bad Request and exits, avoiding processing invalid data.
Prepare the response writerhttp.NewResponseWriter(conn, s.idleTimeout)
creates a ResponseWriter
to handle sending responses back to the client. This abstracts away low-level details like writing headers, status, and body.
Find the route and handlers.FindRoute
looks up the request path and HTTP method in the server’s route table. It returns the correct handler function and any dynamic route parameters.
Apply middlewares
All middlewares are applied to the handler using s.middlewares.Apply(handler)
. This allows pre-processing, such as logging, authentication, or modifying requests/responses before reaching the final handler.
Set request parameters and Keep-Alive
Dynamic route parameters are attached to the request object. The server also checks if the client requested Keep-Alive. If so, it sets the response to keep the connection open for subsequent requests.
Execute the handler
The final handler is called with the request and response. If it returns an error, the server sends the appropriate HTTP response (404 Not Found, 400 Bad Request, 500 Internal Server Error) back to the client.
Close connection if not Keep-Alive
Finally, the function checks the Connection
header. If the client requested keep-alive
, the loop continues to handle more requests on the same connection. If the client wants the connection closed (or did not request Keep-Alive), the loop ends and the connection is automatically closed.
1
2func handleConnection(conn net.Conn, s *Server) {
3 defer conn.Close()
4
5 for {
6 if s.idleTimeout > 0 {
7 _ = conn.SetDeadline(time.Now().Add(s.idleTimeout))
8 }
9
10 req, err := http.ParseRequest(conn)
11 response := http.NewResponseWriter(conn, s.idleTimeout)
12 if err != nil {
13 response.SendBadRequest(err.Error())
14 return
15 }
16
17 handler, params := s.FindRoute(req.RequestLine.Path, req.RequestLine.Method)
18
19 finalHandler := s.middlewares.Apply(handler)
20
21 req.Params = params
22 keepAlive := req.IsKeepAlive()
23 response.SetKeppAlive(keepAlive)
24
25 if routeErr := finalHandler(response, req); routeErr != nil {
26 switch routeErr.Code {
27 case types.NotFound:
28 response.SendNotFound(routeErr.Message)
29 case types.MethodNotAllowed:
30 response.SendBadRequest(routeErr.Message)
31 default:
32 response.SendInternalServerError(routeErr.Message)
33 }
34 }
35 if !keepAlive {
36 return
37 }
38 }
39}
Let’s create some endpoints. We will serve HTML, CSS, and JavaScript files for a login page. If the user enters the correct username, it will show an alert; otherwise, it will display “Invalid username or password.” We will also create a middleware to log each request. as we can see we can register the handler like this first choose the method then the path and the last is the function
1server.Handle(types.GET, "/", sendIndex)
2server.Handle(types.GET, "/style.css", serveStatic)
3server.Handle(types.GET, "/script.js", serveStatic)
4server.Handle(types.POST, "/login", handleLogin)
let's see the function.sendIndex
serves the main HTML page when the root path is requested. serveStatic
serves other static files (CSS, JS, images) from the staticDir
folder, defaulting to index.html
if the path is /
. handleLogin
processes a simple login form: it parses the request body, checks if the username and password match a hardcoded combination, and responds with a JSON message indicating success or failure.
1const staticDir = "/home/mohe/Documents/github/my-server/static"
2
3// Serve root page
4func sendIndex(res *http.ResponseWriter, req *http.Request) *types.RouteError {
5 res.Status = types.OK
6 return res.SendFile(filepath.Join(staticDir, "index.html"))
7}
8
9// Serve static files
10func serveStatic(res *http.ResponseWriter, req *http.Request) *types.RouteError {
11 path := req.RequestLine.Path
12 if path == "/" || path == "." {
13 path = "/index.html"
14 }
15
16 fullPath := filepath.Join(staticDir, path)
17 res.Status = types.OK
18
19 return res.SendFile(fullPath)
20}
21
22// Handle login with simple logic
23func handleLogin(res *http.ResponseWriter, req *http.Request) *types.RouteError {
24
25 values, err := url.ParseQuery(string(req.Body))
26 if err != nil {
27 res.Status = types.BadRequest
28 return &types.RouteError{Code: types.BadRequest, Message: "Invalid form data"}
29 }
30
31 username := values.Get("username")
32 password := values.Get("password")
33
34 if username == "admin" && password == "1234" {
35 return res.SendJSON(map[string]string{
36 "status": "success",
37 "message": "Login successful",
38 }, types.OK)
39 }
40
41 return res.SendJSON(map[string]string{
42 "status": "error",
43 "message": "Invalid credentials",
44 }, types.Unauthorized)
45}
server.Use(LoggingMiddleware)
tells the server to run LoggingMiddleware
on every incoming request before it reaches the final endpoint handler. we explain this method above.
1server.Use(LoggingMiddleware)
This function defines a middleware called LoggingMiddleware
.
It takes a Handler
(the next function in the chain) and returns a new Handler
that wraps it.
When a request comes in, this middleware first logs the request method, path, and HTTP version to the console. After logging, it calls the next handler in the chain (next(w, r)
), so the request continues to its intended endpoint.
1
2func LoggingMiddleware(next internals.Handler) internals.Handler {
3 return func(w *http.ResponseWriter, r *http.Request) *types.RouteError {
4 log.Printf("[%s] %s %s\n", r.RequestLine.Method, r.RequestLine.Path, r.RequestLine.Version)
5 return next(w, r)
6 }
7}
let's run the server and go to the localhost:8080/
.
Let’s test it if we enter the correct username and password, it should show an alert message. Indeed, it works, and we can see the POST request being received by the server.
This sets up a search endpoint that captures a dynamic parameter from the URL. When a request is made to /search/{firstID}
, the Search
handler runs. It retrieves the firstID
value from the route parameters and prints it to the server console. This shows how you can access dynamic URL segments in your handler.
1func Search(res *http.ResponseWriter, req *http.Request) *types.RouteError {
2 firstID, _ := req.Params.Get("firstID")
3 fmt.Println("FirstID:", first)
4 return nil
5}
6
7func main(){
8....
9server.Handle(types.GET, "/search/{firstID}", Search)
10.....
11}
Published Sep 1, 2025
StateDone
)