Pakyow Fundamentals 001: Build an App in 9 LOC

For some time now I've wanted to write about the fundamental concepts behind Pakyow. This is the first of a dozen or so posts covering many of the details and considerations that have made their way into the framework's design.

I also recorded a screencast where I ramble through roughly the same content. It's available on the Pakyow YouTube Channel and is a nice supplement for this post.

Here it is, the simplest app you can build in Pakyow:

require 'pakyow-core'

Pakyow::App.define do
  routes do
    default do
      send 'hello'
    end
  end
end.run

1 library, 3 dependencies, 9 total lines of code.

And yes, you can tweet it.

Save to a file (e.g. app.rb) and run it:

ruby app.rb

See Installation for details on getting your environment setup.

The app server should start up at localhost:3000. This app is nothing complex — it simply accepts a request at the default/ path and returns hello in the response body.

$ curl http://localhost:3000/
hello

Despite its simplicity, there are many things we can learn from this example.

Single Responsibility Libraries

You'll notice that we require pakyow-core rather than pakyow. This is because we only want a portion of the functionality that Pakyow provides. Pakyow consists of 8 different libraries, or gems, each introducing a separate concern.

The primary pakyow gem requires all 8 of the gems. Since we know that this app will only deal with routing, we choose the pakyow-core gem. The core Pakyow gem handles defining the app, routing requests to it, and sending responses.

Single responsibility gems let us start small and only include features in our app when we need them. This keeps apps as lightweight as possible while scaling to meet the requirements of more complex apps (more on this in future posts).

Apps That Grow With You

After requiring core, we define our app.

Pakyow::App.define do
  ...
end

Pakyow is designed to allow every part of the app code to be specified right in the define block. Here we can not only define our routes, but configuration, middleware, and more advanced features like mutators.

While this is an easy place to start, it does start to break down as an app grows larger. Pakyow offers ways to break things out as needed.

Pakyow often provides multiple ways to accomplish the same task. You choose between them based on your needs. For a simple app like this, specifying the routes elsewhere creates more friction than just doing it inline. When the app grows, it's easy to refactor these routes into their own files.

This design decision provides a clear starting point while providing ways to organize code in a larger codebase. You can take it as far as you need to.

Routing Requests

Now that we've defined an app, let's take a closer look at our routes.

default do
  send 'hello'
end

In Pakyow, a route is responsible for routing a request to a bit of logic. Here we see a single route, called default. This route will match a request to GET /, or the default path.

Pakyow couples the definition of the route with the work to be performed when the route is matched. You see this pattern in other frameworks, like Sinatra. We chose this approach to keep routing concerns in one place.

Consider if we had written the route like this:

default :SomeController, :some_action

SomeController#some_action would look like this:

class SomeController
  def some_action
    send 'hello'
  end
end

The call to send is a routing concern. Putting it in the controller (away from routing) requires us to keep two bits of information in mind to understand the entire request / response flow. This is bad.

A pattern I like to use when building Pakyow apps is for routes to contain only routing logic and delegate other concerns. Here's an example:

default do
  send SomeService.hello_or_goodbye
end

Assume that the method on our service object makes some decision about whether it should say "hello" or "goodbye" and returns the appropriate message. We can look at this route and understand everything that happens from request all the way to response. All routing concerns are right in front of us.

Sending Data

Our default route simply sends back hello in the response. This is done through Pakyow's send helper, which accepts a String or IO object (e.g. aFile). It sets the content headers and body of the response, then immediately halts execution and returns the response.

Send is one of a handful of helper methods that provide us with tools for dealing with different routing concerns. We'll look at other helpers in the future including error handlers, redirects, and rerouting.

Choosing an App Server

When your app server started up, it probably booted Webrick. This is the default server in Ruby and is part of the standard library. If Pakyow can't find another app server in its environment, it falls back to Webrick.

You can use a different app server by requiring it:

require 'pakyow-core'
require 'puma'

...

Now the app will boot with Puma instead of Webrick.

Pakyow tries to make sane decisions when things are unspecified, but gives you the power to easily influence or change those decisions. This further reduces friction by letting you deal with things when you want to deal with them, rather than requiring many decisions upfront.

Start with conventions, allow configuration in the future.

Conclusion

We'll build on this 9 line app in the next post and discuss configuration, helpers, and middleware. Thanks for reading!