DEV Community

Cover image for Farewell-to-Framework-Bloat-How-I-Rediscovered-Simplicity-Without-Sacrificing-Performance
member_06022d8b
member_06022d8b

Posted on

Farewell-to-Framework-Bloat-How-I-Rediscovered-Simplicity-Without-Sacrificing-Performance

GitHub Home

I’ve been writing code for over forty years. I started when punch cards were still a thing and the internet was a fever dream in a university lab. I’ve seen languages and frameworks rise and fall like empires. I’ve ridden the waves of hype and seen them crash on the shores of reality. And if there’s one thing I’ve learned, it’s that complexity is the enemy. Not the good kind of complexity, the kind that tackles a genuinely hard problem. I’m talking about the bad kind. The kind that frameworks, in their endless quest for features, pile on until you’re writing more boilerplate than actual business logic.

For the last decade, I felt like I was drowning in that kind of complexity. Every new project, every new team, it was the same story. We’d pick a popular framework—Node.js with Express, Spring Boot in the Java world, Django in Python-land. They all promised rapid development. And they delivered, at first. You could get a "hello world" server up in minutes. But then the real work would begin.

Need custom middleware? That’s a specific function signature you have to memorize, and God help you if you put the arguments in the wrong order. Want WebSockets? That’s another library, another dependency, another layer of abstraction to fight with. Performance tuning? Get ready to dive into a labyrinth of configuration options, garbage collector tuning, and esoteric command-line flags. I found myself spending more time reading framework documentation and fighting with the "magic" it was doing behind the scenes than I did solving the actual problems my users had. My code felt heavy, bloated, and fragile. It was a house of cards built on a foundation of countless NPM packages.

Let's take a simple example. A basic web server in Node.js using Express that has a couple of routes, some middleware to log requests, and a WebSocket endpoint. It’s a common enough requirement. The code would look something like this.

const express = require('express');
const http = require('http');
const { WebSocketServer } = require('ws');

const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });

// Middleware to log every request
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
});

// A simple REST endpoint
app.get('/', (req, res) => {
  res.send('Hello from the old world!');
});

// Another endpoint
app.get('/api/data', (req, res) => {
  res.json({ message: 'This is some data' });
});

// WebSocket connection handler
wss.on('connection', (ws) => {
  console.log('Client connected');

  ws.on('message', (message) => {
    console.log(`Received message => ${message}`);
    // Echo the message back
    ws.send(`You said: ${message}`);
  });

  ws.on('close', () => {
    console.log('Client disconnected');
  });
});

const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Look at it. It’s not terrible, but it’s not great either. We’re pulling in three separate modules right off the bat. We have to manually stitch together the http server with the express app and then again with the WebSocketServer. The middleware is a function with a next callback, a pattern that has caused countless bugs when developers forget to call it. It works, but it feels… cobbled together. This is the bloat I’m talking about. It’s a death by a thousand papercuts.

I was convinced this was just the price of modern web development. I was wrong. A few months ago, a younger colleague, seeing my frustration, quietly suggested I look into a Rust-based framework he’d been toying with for a personal project. I was skeptical. I’ve seen the "next big thing" come and go. But I respect this colleague, so I gave it a shot. The framework was called hyperlane.

I spent a weekend with it. And for the first time in years, I felt a spark of joy. It was like coming home. The design was clean, the APIs were intuitive, and the performance was breathtaking. It didn’t try to be everything to everyone. It focused on being a damn good web server, and it did it with an elegance I hadn’t seen in a long, long time.

I decided to replicate that simple Express server. Here’s what the equivalent code looks like in hyperlane.

use hyperlane::*;

async fn connected_hook(ctx: Context) {
    if !ctx.get_request().await.is_ws() {
        return;
    }
    let socket_addr: String = ctx.get_socket_addr_string().await;
    let _ = ctx.set_response_body(socket_addr).await.send_body().await;
}

async fn request_middleware(ctx: Context) {
    let socket_addr: String = ctx.get_socket_addr_string().await;
    ctx.set_response_version(HttpVersion::HTTP1_1)
        .await
        .set_response_status_code(200)
        .await
        .set_response_header(SERVER, HYPERLANE)
        .await
        .set_response_header(CONNECTION, KEEP_ALIVE)
        .await
        .set_response_header(CONTENT_TYPE, TEXT_PLAIN)
        .await
        .set_response_header("SocketAddr", socket_addr)
        .await;
}

async fn response_middleware(ctx: Context) {
    if ctx.get_request().await.is_ws() {
        return;
    }
    let _ = ctx.send().await;
}

async fn root_route(ctx: Context) {
    let path: RequestPath = ctx.get_request_path().await;
    let response_body: String = format!("Hello hyperlane => {}", path);
    let cookie1: String = CookieBuilder::new("key1", "value1").http_only().build();
    let cookie2: String = CookieBuilder::new("key2", "value2").http_only().build();
    ctx.set_response_status_code(200)
        .await
        .set_response_header(SET_COOKIE, cookie1)
        .await
        .set_response_header(SET_COOKIE, cookie2)
        .await
        .set_response_body(response_body)
        .await;
}

async fn ws_route(ctx: Context) {
    if let Some(send_body_hook) = ctx.try_get_send_body_hook().await {
        while ctx.ws_from_stream(4096).await.is_ok() {
            let request_body: Vec<u8> = ctx.get_request_body().await;
            ctx.set_response_body(&request_body).await;
            send_body_hook(ctx.clone()).await;
        }
    }
}

async fn sse_route(ctx: Context) {
    let _ = ctx
        .set_response_header(CONTENT_TYPE, TEXT_EVENT_STREAM)
        .await
        .send()
        .await;
    for i in 0..10 {
        let _ = ctx
            .set_response_body(format!("data:{}{}", i, HTTP_DOUBLE_BR))
            .await
            .send_body()
            .await;
    }
    let _ = ctx.closed().await;
}

async fn dynamic_route(ctx: Context) {
    let param: RouteParams = ctx.get_route_params().await;
    panic!("Test panic {:?}", param);
}

async fn panic_hook(ctx: Context) {
    let error: Panic = ctx.try_get_panic().await.unwrap_or_default();
    let response_body: String = error.to_string();
    eprintln!("{}", response_body);
    let _ = std::io::Write::flush(&mut std::io::stderr());
    let content_type: String = ContentType::format_content_type_with_charset(TEXT_PLAIN, UTF8);
    let _ = ctx
        .set_response_version(HttpVersion::HTTP1_1)
        .await
        .set_response_status_code(500)
        .await
        .clear_response_headers()
        .await
        .set_response_header(SERVER, HYPERLANE)
        .await
        .set_response_header(CONTENT_TYPE, content_type)
        .await
        .set_response_body(response_body)
        .await
        .send()
        .await;
}

#[tokio::main]
async fn main() {
    let config: ServerConfig = ServerConfig::new().await;
    config.host("0.0.0.0").await;
    config.port(60000).await;
    config.enable_nodelay().await;
    config.http_buffer(4096).await;
    config.ws_buffer(4096).await;
    let server: Server = Server::from(config).await;
    server.panic_hook(panic_hook).await;
    server.request_middleware(connected_hook).await;
    server.request_middleware(request_middleware).await;
    server.response_middleware(response_middleware).await;
    server.route("/", root_route).await;
    server.route("/ws", ws_route).await;
    server.route("/sse", sse_route).await;
    server.route("/dynamic/{routing}", dynamic_route).await;
    server.route("/regex/{file:^.*$}", dynamic_route).await;
    let server_hook: ServerHook = server.run().await.unwrap_or_default();
    server_hook.wait().await;
}
Enter fullscreen mode Exit fullscreen mode

The difference is night and day. Everything is built-in. WebSockets and Server-Sent Events aren't afterthoughts; they are first-class citizens. The entire server is configured and run through a single, coherent Server object. The middleware and hooks are just async functions that are handed a Context object. There's no next callback to forget. You just... write your code. The fluent API for building the server and the responses is a joy to use. It guides you toward the right way of doing things.

And the performance? It’s not even a fair comparison. A compiled Rust binary running on Tokio will run circles around a JIT-compiled, garbage-collected language like JavaScript. It uses a fraction of the memory and can handle significantly more concurrent connections on the same hardware. This isn't just a theoretical benchmark; it's something you can feel. The responses are snappier, the latency is lower, and the whole thing is just more stable. You're not going to get a call at 3 AM because a memory leak in a third-party library crashed the server.

The extensibility is where hyperlane truly shines for me. The hook system is brilliant. You have hooks for when a client connects (connected_hook), hooks for when a panic occurs (panic_hook), and a clear, well-defined middleware pipeline. It gives you these precise, surgical insertion points to add functionality. You don't need to wrap your entire application in layers of middleware just to add a simple logger. You can inject your logic exactly where it needs to go. This makes the code cleaner, easier to reason about, and vastly more maintainable.

I feel like I've spent a decade building skyscrapers with LEGOs, and someone just handed me a set of perfectly machined, industrial-grade tools. hyperlane isn't just another framework. It's a philosophical statement. It's a belief that you can have performance, safety, and a world-class developer experience all at the same time. It’s a return to simplicity, and I, for one, am never going back.

GitHub Home

Top comments (0)