Life Without Frameworks: Hyper

When it comes to building an HTTP server in rust, there are a lot of available frameworks that provide some very common things like routing and upload handling. You have small ones like warp and tide. Then the large ones, rocket and actix-web. There are also narrow ones like tonic that strictly focus on gRPC. There are so many it's hard to even choose. How can there be so many, when you can't even tell some of them apart?

With the exception of tide, most of these frameworks rest on hyper for handling the HTTP traffic. I'm not going to rave that hyper is some mind-exploding feat of web tech, but that it backs most of these frameworks under the hood is certainly testament to its power as a sub-layer for building a web server. Working with it directly, you may have to roll your own components, sure, but you could also gain a lot of simplicity in your project code.

Features You Might Not Need

What follows is a very opinionated breakdown of the features of these large frameworks and why you might be able to live without them.

Routing

Probably the first thing you'll notice when checking out the hyper guides and documentation, is that there is no magical route handling[1].

match (req.method(), req.uri().path()) {
    (&Method::GET, "/") => {
        *response.body_mut() = Body::from("Try POSTing data to /echo");
    },
    (&Method::POST, "/echo") => {
        // we'll be back
    },
    _ => {
        *response.status_mut() = StatusCode::NOT_FOUND;
    },
};

Compare that with Rocket's attribute routing:

#[get("/hello/<name>/<age>")]
fn hello(name: &str, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}

The Rocket one is definitely easier to read and it really might be what you need exactly. The other frameworks handle routing in similar ways, typically parsing route paths as some domain-specific language (DSL) or at least providing some builder pattern to assemble the path, the method, and the parameters into a recognizable access point.

My problem with this isn't necessarily Rocket's handling of routes like this, but the general idea of REST-style routing. It's probably an entire article on its own to express my true feelings about REST APIs, but I can simply say that I've grown weary of passing unstructured information through the URL when I can simply deliver structured data as body content.

If dynamic route parsing is still important to you, there's no reason you can't bring that in on its own, like with routerify. Even still, what routerify is doing isn't that sophisticated.

Cookies

If you are building a general API server, please do not handle sessions at this level. While there's no reason to get all crazy with microservices right away, you may potentially consider deploying your API behind a load balancer. Among other things you'll have to deal with, persisting sessions from node to node is not going to be easy. You will want to dedicate session management somewhere else like a Redis database.

That these frameworks provide cookie management like we're still in the days of PHP serving one application on one node is almost irresponsible and leads developers to building monolithic applications that tend to do too much and can't be scaled. Cookies may certainly have some value (probably not for an API), but you are going to be better served by isolating cookies in a different place like for handling authentication sessions or user tracking.

File Uploads

On the surface, an HTTP framework really just needs to accept a file upload and persist to the file system in order for your route handler to go to work. In my experience, file handling is rarely so simple. What if the file is really large? Can your request accept file chunks? Where are temporary files stored? Might they be cleaned up by the operating system before you get a chance to deal with it?

There is just a lot of work to be done to accept files through your API. No framework is going to handle all the cases. You're going to have to get your hands dirty. Might as well start on day one of the project.

I currently recommend multer. It handles multipart/form-data requests and it's fairly straightforward to integrate with hyper.

Middleware

If there is something hyped up more than anything else about web frameworks, it's middleware. Why? It's like the aspect-oriented programming (AOP) of request handling. Everyone thought that one blanket paradigm that was able to hook into any part of the code whenever necessary was somehow going to pay dividends for keeping global logic from spilling into the actual request handling. Well, surprise! It usually spills over.

Authorization is one of those things that you'd think you could just lock away in some middle layer, but it's just never quite so clean. You frequently have to handle the request somewhat like retrieve stuff from the database to detect the user's ability to complete the request. That's not something a middleware pattern is going to improve. Even if you can isolate enough of that logic from the actual request handling (i.e. logging), then it's usually not that complex and didn't really need some over-engineered pipeline pattern to handle.

Form Handling

Submitting forms directly to a server these days is becoming a bit of a lost art. First, you really just can't get away without using JavaScript with forms. For validation or a clean user experience, you have to make forms as straightforward for users as possible and straight HTML is not going to cut it. So if you're already using JavaScript, why not send the data as JSON, huh? Then, should you need to use the server API from an application that isn't a web form, you already support it. Currently, serde is the foremost crate for handling JSON and is quite capable for handling structured request data.

Templating

Wait, so you're actually going to dynamically render HTML on the server and send it over HTTP? That's crazy talk. Even if you don't want to use one of the dozen client-side rendering JavaScript frameworks, you're going to want to dynamically load partial content on-the-fly (read: AJAX) without having to re-send all of the content for each page. Browser caching only gets you so far and well-designed web interfaces are complex with a lot of styles and scripting. Until things change, it's just simpler to stick to rendering content on one side or the other. If you can, render things statically on the server or otherwise render it dynamically on the client.

Question the Framework

A lot of my disdain for these abstractions probably comes from having a bad experience with Ruby on Rails. All the magic and DSLs may offer a sense of robustness and ability to easily pivot by adding whatever features you may need down the road. This is very appealing for agency work where you build custom site after custom site for clients without centering around a single product. The promises of a framework, that things are easy to get started, all-inclusive, and extensible, may be true to some degree, but that comes with a lot of penalties that are possibly glossed-over and ignored.

I think this is endemic to the industry of web development as a whole. At some point, jobs were listed for "React Developers" instead of "JavaScript Developers" or whatever the trendy framework du jour is. Code camps further this by training new recruits in just React and bypass some of the fundamentals of handling the DOM or how JavaScript closures work. This might lower the barrier to entry, but there are always cases where you need to leave the framework to build something custom. Don't hire a programmer who doesn't know the programming language. It really shouldn't have to be said.

From DotNet to Flutter, frameworks are everywhere and along with them entire fleets of recruiters looking for code monkeys to push out brackets and semi-colons without much questioning or push-back. Of course, a job is a job and sometimes a framework is required. However, when you're in a position to choose the framework, it wouldn't hurt to stop and weigh the options. Can you get away with rolling your own implementations that are better suited to your project's requirements? Is it worth abstracting away some of the core components of your work if someday you might need to extend those inner pieces? Are you willing to commit your whole team (and future team) to using this framework? Is this framework going to outlast its competitors?

Just ask the questions. Ask them.


  1. Well, that's not the first thing you notice. It's hard to get past hyper's self-proclaimed warnings of being a "low-level" library. ↩︎