DEV Community

Cover image for Building a Web Server in Pure Rust
Derek mwale
Derek mwale

Posted on

Building a Web Server in Pure Rust

The first web server I ever built wasn't written in Rust.

It wasn't even close.

Like many backend developers, I started with frameworks.

Django.

Laravel.

Express.

Frameworks made everything feel effortless.

Define a route.

Return a response.

Connect a database.

Deploy.

Within minutes, an application was online.

It felt like magic.

Then one day I asked myself a different question.

"What is the framework actually doing?"

That simple question led me down one of the most rewarding learning experiences in backend engineering.

Instead of relying on a framework, I wanted to build a web server from scratch.

No routing libraries.

No HTTP frameworks.

Just Rust, the standard library, and a curiosity about what really happens when a browser sends a request.

What I discovered wasn't just how HTTP works.

I discovered how operating systems, networking, concurrency, parsing, memory management, and software architecture all come together to create something we casually call a web server.

Let's build one.

Not because production applications should avoid frameworks.

But because understanding the layers beneath them makes every backend engineer stronger.


What Is a Web Server?

Before writing any code, we should answer a simple question.

What does a web server actually do?

Surprisingly, the answer is short.

It waits.

More specifically, it waits for network connections.

When one arrives, it:

  • Accepts the connection.
  • Reads the incoming data.
  • Understands the HTTP request.
  • Decides what response to return.
  • Sends the response.
  • Closes—or keeps—the connection.

That's it.

Everything else is refinement.


Understanding HTTP

Suppose your browser requests a homepage.

The browser doesn't send magic.

It sends text.

GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0
Accept: */*
Enter fullscreen mode Exit fullscreen mode

Every request follows a predictable structure.

Request line.

Headers.

Optional body.

A web server simply reads this information and decides how to respond.

Likewise, responses are just text.

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 18

<h1>Hello Rust</h1>
Enter fullscreen mode Exit fullscreen mode

HTTP feels much less mysterious once you realize it's largely a carefully structured conversation.


Designing the Architecture

Rather than placing everything inside main(), let's separate responsibilities.

Browser
      │
      ▼
 TCP Listener
      │
      ▼
 Connection Handler
      │
      ▼
 HTTP Parser
      │
      ▼
 Router
      │
      ▼
 Response Builder
Enter fullscreen mode Exit fullscreen mode

Each component has one responsibility.

This simple design makes the project easier to understand, test, and extend.


Listening for Connections

Everything begins with a socket.

Rust's standard library provides TcpListener.

use std::net::TcpListener;

let listener = TcpListener::bind("127.0.0.1:8080")?;
Enter fullscreen mode Exit fullscreen mode

The operating system now listens on port 8080.

Nothing has happened yet.

The server simply waits.

One lesson backend engineering teaches repeatedly is that servers spend most of their lives waiting.

Waiting for requests.

Waiting for databases.

Waiting for files.

Waiting for other services.

Good software uses that waiting time wisely.


Accepting Clients

Once a browser connects, the listener produces a TCP stream.

for stream in listener.incoming() {
    let stream = stream?;
    handle_connection(stream);
}
Enter fullscreen mode Exit fullscreen mode

Notice how clean the logic is.

The listener doesn't understand HTTP.

It simply accepts network connections.

The handler does the rest.

Separating responsibilities keeps complexity under control.


Reading the Request

A TCP stream is just bytes.

We must transform those bytes into something meaningful.

use std::io::Read;

let mut buffer = [0; 4096];
stream.read(&mut buffer)?;
Enter fullscreen mode Exit fullscreen mode

Now we have raw data.

The next task is interpretation.

Programming often follows this pattern.

Receive raw information.

Transform it into useful structures.

Operate on those structures.


Parsing HTTP

Instead of passing strings throughout the application, create a dedicated type.

struct Request {
    method: String,
    path: String,
    version: String,
}
Enter fullscreen mode Exit fullscreen mode

The parser extracts:

  • HTTP method
  • Requested path
  • HTTP version

Later we can add headers, cookies, query parameters, and request bodies.

Small, incremental improvements produce maintainable software.


Routing Requests

Every web framework includes a router.

We can build a simple one ourselves.

/

Home Page

/about

About Page

/users

Users
Enter fullscreen mode Exit fullscreen mode

One straightforward approach uses a match statement.

match request.path.as_str() {
    "/" => home(),
    "/about" => about(),
    _ => not_found(),
}
Enter fullscreen mode Exit fullscreen mode

Elegant.

Readable.

Easy to expand.

Routing is simply choosing the correct function based on a URL.


Building Responses

Instead of manually formatting responses everywhere, create a response type.

struct Response {
    status: u16,
    body: String,
    content_type: String,
}
Enter fullscreen mode Exit fullscreen mode

This structure hides formatting details from the rest of the application.

Every route simply returns a response.

The response knows how to serialize itself into HTTP.

Abstraction reduces duplication.


Writing Back to the Client

After constructing the response:

use std::io::Write;

stream.write_all(response.as_bytes())?;
Enter fullscreen mode Exit fullscreen mode

The browser immediately interprets the bytes.

If everything worked correctly, the webpage appears.

It's remarkable how much software exists between typing a URL and seeing a page load.


Concurrency Changes Everything

Our first server processes one request at a time.

Imagine one request takes five seconds.

Everyone else waits.

That's unacceptable.

Instead, create a thread for each connection.

std::thread::spawn(move || {
    handle_connection(stream);
});
Enter fullscreen mode Exit fullscreen mode

Now multiple users can connect simultaneously.

The architecture immediately becomes more responsive.


Thread Pools Scale Better

Creating a new thread for every request eventually becomes expensive.

Threads consume memory.

Scheduling thousands of them wastes CPU time.

A thread pool solves this elegantly.

Incoming Requests
        │
        ▼
     Job Queue
        │
        ▼
 Worker Threads
Enter fullscreen mode Exit fullscreen mode

Instead of creating new workers repeatedly, reuse existing ones.

This pattern appears throughout backend engineering.

Reuse beats recreation.


Designing the Job Queue

The thread pool revolves around one idea.

Tasks wait until workers become available.

A queue naturally models this behavior.

Request A

Request B

Request C
Enter fullscreen mode Exit fullscreen mode

Workers remove requests one by one.

Queues smooth traffic spikes and improve resource utilization.

Choosing the right data structure often simplifies the entire design.


Error Handling

Real clients send imperfect requests.

Malformed HTTP.

Unsupported methods.

Missing headers.

Unexpected input.

Rather than crashing, return meaningful responses.

400 Bad Request
Enter fullscreen mode Exit fullscreen mode

or

404 Not Found
Enter fullscreen mode Exit fullscreen mode

or

500 Internal Server Error
Enter fullscreen mode Exit fullscreen mode

Reliable software communicates failure clearly.


Serving Static Files

Suppose someone requests:

/images/logo.png
Enter fullscreen mode Exit fullscreen mode

Instead of generating HTML dynamically, simply read the file.

Disk

↓

Memory

↓

HTTP Response
Enter fullscreen mode Exit fullscreen mode

Now the web server becomes capable of serving websites, images, CSS, and JavaScript.

A surprisingly useful milestone.


Logging Requests

Sooner or later you'll ask:

"Why isn't this endpoint working?"

Logs answer that question.

Record:

  • Client IP
  • Request path
  • Response status
  • Processing time

Logs become the server's memory.

Without them, debugging production systems becomes guesswork.


Configuration

Avoid hardcoding values.

Instead create a configuration structure.

struct Config {
    host: String,
    port: u16,
}
Enter fullscreen mode Exit fullscreen mode

Configuration allows one binary to run in development, staging, and production without changing code.

Flexibility belongs in configuration.

Behavior belongs in code.


Graceful Shutdown

What happens when the application stops?

A robust server doesn't terminate immediately.

Instead:

  • Stop accepting new connections.
  • Finish active requests.
  • Close resources.
  • Exit cleanly.

Graceful shutdown protects users and prevents interrupted work.

It's one of those invisible features users only notice when it's missing.


Performance Considerations

As traffic grows, bottlenecks appear.

Reading files repeatedly.

Allocating memory unnecessarily.

Blocking on slow operations.

Repeated string parsing.

Premature optimization rarely helps.

Measurement does.

Profile first.

Optimize second.

Rust already provides strong performance characteristics.

Thoughtful architecture amplifies them.


Security Matters

Even a simple server should think defensively.

Validate input.

Limit request sizes.

Prevent directory traversal.

Reject malformed requests.

Set sensible timeouts.

Secure software begins with cautious assumptions.

Never assume clients behave perfectly.


Testing the Server

Before deploying, test scenarios like:

  • Valid requests
  • Missing routes
  • Large payloads
  • Concurrent clients
  • Invalid HTTP
  • Unexpected disconnects

Testing isn't merely proving success.

It's discovering failure safely.


Lessons Beyond Web Servers

Building a web server taught me much more than HTTP.

It revealed how operating systems expose networking primitives.

How protocols become structured conversations.

How concurrency influences architecture.

How data structures simplify system design.

How abstraction reduces complexity.

Most importantly, it reminded me that every framework is ultimately built from simpler ideas.

Understanding those ideas makes frameworks feel less magical and far more logical.


Experience Changed My Perspective

Earlier in my career, I admired frameworks.

Today I admire foundations.

Frameworks save time.

Foundations build understanding.

Knowing how HTTP works helps regardless of language.

Understanding sockets helps whether you're writing Rust, Go, Java, Python, or C#.

Concepts outlive tools.

That's one of the most valuable lessons software engineering continues teaching me.


Final Thoughts

Building a web server in pure Rust isn't about replacing mature frameworks like Axum, Actix Web, or Rocket.

Those frameworks exist because thousands of engineers have already solved difficult problems involving routing, concurrency, middleware, security, and performance.

The real value of this project lies elsewhere.

It reveals what happens beneath the abstractions we rely on every day.

A browser sends bytes across a network.

A TCP listener accepts a connection.

A parser transforms raw data into a request.

A router chooses the correct handler.

A response builder creates structured HTTP output.

Worker threads process requests concurrently.

Logs capture history.

Configuration adapts the server to different environments.

Graceful shutdown protects active users.

Each piece is surprisingly small.

Together they become something powerful.

That, to me, is one of the most beautiful aspects of software engineering.

Complex systems rarely emerge from one brilliant idea.

They emerge from many simple components working together with clearly defined responsibilities.

Rust makes this especially enjoyable because it encourages careful thinking about ownership, concurrency, error handling, and performance from the very beginning.

Those constraints don't limit creativity.

They shape better engineering habits.

The longer I build backend systems, the less interested I become in hiding complexity behind abstractions and the more interested I become in understanding the foundations beneath them.

Because frameworks will continue evolving.

Libraries will come and go.

But the principles behind a web server—networking, protocols, concurrency, architecture, and clean design—remain timeless.

And once you understand those principles, every web framework suddenly becomes much easier to appreciate, extend, and use with confidence.

Top comments (0)