Creating an API with Handy-Httpd

Written on , by Andrew Lalis.

Introduction

When I started programming in the D language, I was coming fresh from the world of Java and Spring, where setting up a new web API for a project was pretty much as simple as creating a new method annotated with @GetMapping. In the D world, the closest thing to an all-encompassing web framework is Vibe.d, which is quite convoluted (from a beginner's perspective), and is heavily focused on its own ecosystem and async implementations. That gives it a sort of "walled-garden" feel. There were other, smaller HTTP web server libraries, but none were well documented. So in the end, I set out to build my own HTTP server, which then evolved into more of an HTTP web framework. That's Handy-Httpd.

Handy-Httpd was built on the concept of an HttpRequestHandler, a single unit that takes an HTTP request and does stuff with it.


				class ExampleHandler : HttpRequestHandler {
					void handle(ref HttpRequestContext ctx) {
						ctx.response.writeBodyString("Hello world!");
					}
				}
			
Here's an example of an HttpRequestHandler that just responds with "Hello world!" to any request.

With this request handler, and the principle of composition, we can build up a complete web framework from a few well-purposed handlers.

Setting Up

As with any web server, there's some startup and configuration that needs to be done to get everything working, so we'll get that out of the way first.

We need to create a new D project, and add handy-httpd as a dependency. Do so with the dub CLI tool, or however you prefer to create D projects. Then we'll set up the basic HTTP server in our project's main file.


				import handy_httpd;
				import handy_httpd.handlers.path_delegating_handler;

				void main() {
					ServerConfig config = ServerConfig.defaultValues();
					config.workerPoolSize = 3;
					PathDelegatingHandler pathHandler = new PathDelegatingHandler();
					// TODO: Add mappings to pathHandler
					HttpServer server = new HttpServer(pathHandler, config);
					server.start();
				}
			

The typical boilerplate consists of three main things:

  1. Configuration
  2. Building our request handler
  3. Starting the server

For illustrations' sake, I've configured this server to use 3 workers in its pool for handling requests. You might need more depending on your traffic. I've also created a new PathDelegatingHandler which will serve as the basis for the API's set of endpoints. Check out the documentation on this handler for a detailed explanation of what it can do; in short, we can register new API endpoints to it.

Adding an Endpoint

Now that our server is set up, all we need to do is define some endpoints for users to interact with. This is as simple as creating an HttpRequestHandler and registering it with our pathHandler that we defined on line 7.

To keep things simple to start, we'll add a status endpoint that just returns the string "online". For something this basic, there's no need to create a whole new class; instead, we'll just define a function.


				void handleStatus(ref HttpRequestContext ctx) {
					ctx.response.writeBodyString("online");
				}
			

And then, we'll register it with our path handler so that GET requests to /status will be directed to the handleStatus function.


				pathHandler.addMapping(Method.GET, "/status", &handleStatus);
			

Done! We can now run our project and navigate to localhost:8080/status, and we should see the text "online". It's that simple.

Posting Data to Our API

A GET endpoint is easy enough, but making an endpoint that accepts the user's data isn't too hard either.


				void receivePost(ref HttpRequestContext ctx) {
					JSONValue content = ctx.request.readBodyAsJson();
					// Do stuff with the content.
				}
			

You're not limited to only JSON though; users can upload URL-encoded data, or XML, or literally anything else. It's just that D's standard library provides a JSON implementation, so Handy-Httpd gives you some help with it.

Under the hood, Handy-Httpd uses the streams library for the underlying data transfer, so if you're looking for a completely custom solution, you'll need to read from ctx.request.inputStream, which is a InputStream!ubyte. Also note that each request has a pre-allocated receiveBuffer that you can use instead of creating your own separate buffer.

Adding Middleware with Filters

One of the buzz words these days in web programming is "middleware", which is just a fancy term for anything that sits between two systems and performs some limited set of functions on the data that is passed between the systems.

In Handy-Httpd, we've provided a convenient method of adding middleware to the HTTP request handling flow with the HttpRequestFilter, FilterChain, and the FilteredRequestHandler.

Handy-Httpd request filter diagram
An illustration of how an HTTP request is processed by a FilteredRequestHandler: first it goes through all pre-request filters, then the underlying handler, and finally any post-request filters.

Suppose we want to authenticate requests to certain endpoints. That's a pretty straightforward task that many web frameworks deal with. In Handy-Httpd, you'd create a filter that reads a JWT token from the request's header, decodes it, and adds user info to the request context's metadata property.


				class TokenFilter : HttpRequestFilter {
					void apply(ref HttpRequestContext ctx, FilterChain filterChain) {
						Nullable!UserInfo userInfo = tryParseToken(ctx.request.getHeader("Authorization"));
						if (userInfo.isNull) {
							ctx.response.setStatus(HttpStatus.UNAUTHORIZED);
							ctx.response.writeBodyString("Invalid or missing token.");
							return; // Exit without continuing the filter chain.
						}
						ctx.metadata["userInfo"] = userInfo.get();
						filterChain.doFilter(ctx);
					}

					private Nullable!UserInfo tryParseToken(string authHeader) {
						// Actual implementation omitted for brevity.
					}
				}
			

Then to actually use your newly-created TokenFilter to safeguard your endpoint, you'd use the FilteredRequestHandler to wrap your endpoint and set the TokenFilter as one of the pre-request filters.


				FilteredRequestHandler frh = new FilteredRequestHandler(
					mySuperSecretEndpoint,
					[new TokenFilter()]
				);
			

That's all there is to it! No runtime magic, just composing a handler that does exactly what you tell it to.

Additional Remarks

While I've done my best (which admittedly isn't that good) to keep Handy-Httpd lightweight and performant, that really isn't the number 1 goal. My goal was always to build a simple HTTP server with some nice-to-have conveniences that don't require months of diligent study to use effectively. I wanted something with sufficient documentation so that Handy-Httpd can be a D programmer's first introduction to HTTP servers.

We're now on version 7.10.4 as of writing this article, and I feel like Handy-Httpd is in a place where I can comfortably say that it's reached those goals. Thanks for reading all the way through, and if you do try out Handy-Httpd, please do let me know what you think!

Back to Articles