1024px-Flatirons_Winter_Sunrise_edit_2

Nicholas CloudNicholas Cloud is a Software Architect at appendTo and has a real passion for deconstructing business problems, creating abstract, reusable and flexible solutions, and creating awesome software that rocks. Nicholas writes about Strata.js, a node.js server, which makes it easy for him to write tests while developing applications for appendTo clients and projects and having those tests easily ran with a node.js server instead of requiring separate server installations to be set up per developer.

Web servers deliver all the wonderful content on the internet to our web browsers. Web developers write software that makes this content usable. (What would cat videos do without YouTube?) Different platforms and different languages rely on different web servers. In the Microsoft world, .NET developers write ASP.NET applications for the IIS web server. In the open source world, many developers use Apache or nginx to write applications in languages like python, PHP, and ruby.

The de facto web server in the JavaScript/node.js community is probably Express. For simple HTTP operations the standard node http module is often adequate, but web application frameworks like Express add layers of additional functionality on top of the primitive communication channels that the http module provides. Express has a lot of community mindshare from which comes a hearty amount of middleware, extensions, examples, etc. If you’re a node.js (web) developer, there’s a good chance you’ve already been exposed to Express.

There’s also a good chance that you’ve not heard about strata.js, a somewhat obscure node.js web server developed by Twitter employee Michael Jackson. Strata functions like Express, but its exensible middleware model and native streaming features earn it a place of distinction.

Getting started

To stand up a strata web server, create a directory for your application and install the strata npm dependency.

Create a server file in the application directory. I typically call mine httpd.js.

Start your application in the terminal by invoking the node command and passing the server script as its argument.

When you browse to http://localhost:8080 in your browser, you should receive the friendly greeting hello, world!.

There are several things to notice about this example:

  • The strata.run() function starts the web server and accepts a number of arguments. In this example, we provide a function to handle HTTP requests and an options hash, which specifies a specific port.
  • By default, strata listens to port 1982 (Michael Jackson’s birth year!). If we omit the options hash passed to strata.run(), strata will use 1982.
  • There are no routes in this example. All requests will be handled by the function passed to strata.run(). We will add routes later, but this illustrates how strata handles requests for which it can find no matching routes.
  • The signature of the function passed to strata.run() is important. It accepts an env object which contains request (_env_ironment) information, and a cb callback function which must be invoked (either manually, or in this case by passing it to an instance of strata.Result) to generate a response. Any function with the signature function (env, cb) {} in strata is referred to as a strata app.

Routes

Let’s add some routes to our application.

In this example we’ve added an HTTP GET route for the url /house/:surname. Route parameters are prefixed with a colon and will be available as properties on the env.route object. As in our previous example, the route handler conforms to the strata app specification. The default request handler passed to strata.run() has been modified to return an error to the client when the requested route cannot be found. The default handler will only execute if no other route handles the request.

In the default handler we send the response back to the client by creating an instance of strata.Request, but in the GET handler we invoke the callback directly with three arguments: a status code, a headers hash, and a string (or Buffer) of body data. Direct callback invocation is usually favored when responses are simple. When lots of additional response information will be provided (e.g., custom headers, cookies, session data) it is often easier to set properties or call specific methods on a strata.Response object. In either scenario the result is the same.

When the user queries this route with a :surname parameter that matches a key in the houses object, the corresponding family information will be passed back to the client as JSON. If no match is made the user receives a 404 response. We can add data to this object by implementing a POST route.

With the POST route we pass data as a JSON object in the POST body. To capture and automatically parse this data we create an instance of strata.Request and call its body() method. This method invokes a callback with a potential error as its first argument and, in this case, a JSON object as its second argument. (If the Content-Type of the request had been something other than application/json, the second argument would different. Because strata understands that the body is supposed to be a JSON object, though, it parses it automatically.) We then modify the houses object and append the data passed to this route. A 201 Created responses is sent back to the client.

strata.Request also provides a query() method for reading query string parameters, as well as a params() method for parsing parameters from both the query string and body (for URL encoded form POSTs). For multi-part file uploads, strata puts information about the uploaded file in the array of parameters returned by params() (such as path, type, name, size, etc.). This information can be used to perform file system operations on the uploaded file.

You can create routes for all common HTTP request methods, including PUT, DELETE, and HEAD.

Middleware

By now we’re tired of specifying content types and content lengths in our responses, and we’re wondering if strata has a way to make our routes a little less noisy. The answer is: yes, strata provides handy middleware that can infer certain things about our responses. We can modify our GET example to take advantage of this middleware.

Strata supplies strata.contentType and strata.contentLength as middleware to be added to the request/response pipeline by invoking strata.use(). Both of these are functions that examine responses and manipulate them before they are sent to the client. They infer certain things about the response (the content type and the length) from the response body. strata.contentType can use an optional “default” content type, which is passed as the second parameter to strata.use(), and in this example, is text/html (since most of the responses we send are plain text).

In the GET handler we’re still required to pass an empty header object when our error condition occurs, even though the middleware will set the appropriate headers later. When we return actual data, though, we need to manually set the Content-Type because strata interprets strings as plain text and we are actually returning JSON. (Performing a JSON.parse() operation in a try/catch block for every response would be non-performant, so strata does not make the attempt.) We did not have to supply the length for this response though; the middleware will handle that for us.

Static files

Strata can also serve static file content with strata middleware. There are two methods for serving content: using strata.file, which maps static files directly to URLs, or strata.directory which creates a directory listing for files in a particular directory.

When using strata.file, the second argument is the directory from which static content will be served: in this case $APP_ROOT/public. Every path under this directory will be treated as relative to http://localhost:1982. The third (and optional) argument is the default file to render when the URL maps to a directory instead of a file. In this case we use the common index.html file.

The strata.directory middleware works in a similar fashion except it generates its own index page on which it enumerates the contents of a given directory with a link to each file.

Custom middleware

Because strata middleware are simple functions, it is easy to write custom middleware that will be evaluted with every request/response. Strata middleware are executed in the order they are registered with strata.use(): first-to-last for a request, and last-to-first for a response. Defining a custom middleware requires us to create a factory function that returns a middleware “app”. This function accepts two arguments: an “app” that may be a previously registered middleware function or the entry point for the HTTP request, and a configuration hash that provides necessary options for the middleware.

In this example we specify a month in which a special header is to be appended to the response (in this case, June) as an option to our middleware. If the current month is equal to the configured month, we append the x-slogan header to the response. The function nesting can be daunting, but conceptually it works like this:

The middleware factory function captures the next “app” (either another middleware, or the route handler itself) and its config in a closure, then returns its own “app” to be chained to the next middleware that is added to strata. Each middleware app will be given the opportunity to examine and manipulate each request (“upstream” from the route handler) and examine and manipulate each response (“downstream” from the handler) in turn. A middleware may interrupt the request by invoking its callback immediately or may continue processing the request by passing it to the next “app”. Either way, each “app” ends its task by invoking the callback, just as a route would.

Request and response streams

In our POST example, we instantiated a strata.Request object and called the body() method to retrieve JSON data. Calling this method is necessary because, unlike other node.js web servers, strata streams request and response data using node.js Stream objects. When read() is called the strata.Request object accesses the env.input property which is an instance of Stream in a paused state. When the stream is resumed it emits events each time it buffers some data, and when it has finished buffering all data, emits an end event. The strata.Request object listens to these events and concatenates the contents of all evented buffers when the stream has ended. The final content is then passed to the callback to be handled in the route.

We can actually see this in action by modifying our POST example to read the body from the raw input stream.

The benefits of this streaming approach become far more obvious when we want to deliver a large response to the client. Most fantasy nerds cannot resist the allure of a colorful, detailed map of their favorite fantasy world, and we would be remiss if we did not provide one in our application. In our application’s images directory there is a 5.6MB JPEG file called got-map.jpg for which we will create a route. To prevent our web application from being greedy with resources, we will stream the image to the client in chunks instead of loading the whole image into memory first.

(For this example I removed strata.contentType and strata.contentLength middleware because they were ignoring my custom Content-Type and Content-Length headers. This is probably a bug.)

If the file doesn’t exist, we return a simple 404. If it does exist, we stat the file to determine its size (which we send as a header value). We then use the fs.createStream() function to set up an instance of ReadStream that strata will resume when it is ready to begin sending the response. We attach our own callback to the stream’s data event to write out the number of times data is read from the image. On my machine, this event fires 137 times. Since the image is 5,581,413 bytes, this means that strata is delivering 40,740.24 bytes to the client during each data event. For plain text, HTML, and JSON data, streaming usually isn’t a big deal. For images, movies, or large binary files like ISOs, it is essential for server performance and and will prevent request timeouts (since the data isn’t cached in memory all at once).

streaming the map to the client

Other features

Although its extensible middleware and streaming support are strata’s distinguishing characteristics, it provides many other useful features that are powerful tools for any web application.

custom errors and automatic error handling

Strata will handle general errors for you and return a 500 Internal Server Error response, but you can also create your own custom errors that can be returned to the client with different HTTP status codes for specific scenarios. Strata also allows you to override its automatic error handling routine entirely for maximum control.

sessions and cookies

Session cookies persist across requests and can be a convenient way to store temporary information. Strata provides middleware that enables this feature.

custom redirects and forwards

Strata can redirect or forward requests to other URLs. Forwarded URLs can be redirected after some action is taken, e.g., a user attempts to visit a secured page and is forwarded to a login form, then redirected back to the secured page after authentication.

URL rewriting

Strata can magically convert one URL to another. With rainbows and unicorns.

gzip encoding

If your requests and responses are fat, strata can put them on the gzip diet. Using compression is a good way to optimize your web application.

content negotiation

Strata can help you determine if your client will actually accept the data types you are sending to it.

Resources

If you would like to learn more about strata (as you certainly will), consider visiting these fine resources!