Creating a Cookoo Web App (in 23 Lines of Code)

Nov 22 2013

Cookoo is a Chain-of-Command (CoCo) app-building framework written in Go. In this article I explain how to quickly build a Go web server using Cookoo.

By the end of this article we will have a web server with a template engine, and we'll know how to go from here to a more complex web app.

Setup

As with any Go app, the first thing to do is create a new package in the appropiate place under your $GOPATH:

$ cd $GOPATH/src
$ mkdir example
$ cd example

If you haven't done so already, you'll also need to grab the Cookoo package:

$ go get github.com/Masterminds/cookoo

Create a Web Server

The next thing to do is create a simple web app from Cookoo. We'll call it server.go. Here's what it looks like:

package main

import (
    "github.com/Masterminds/cookoo"
    "github.com/Masterminds/cookoo/web"
)

func main() {
    registry, router, context := cookoo.Cookoo()

    web.Serve(registry, router, context)
}

There are a few things to point out:

  • We import two Cookoo packages. The main one (cookoo) provides the main Cookoo features. The second one (cookoo/web) provides the web server.
  • In the main() function we do only two things: we create our new Cookoo app with cookoo.Cookoo(), and then we start a web server with web.Serve().

At this point you have a functioning web server. You can run it with go run:

$ go run server.go

And then you can access it on http://localhost:8080. Since we haven't assigned any routes, you will get a 404 error if you try to run hit that URL.

Let's add a simple route.

A First Route

Like other popular web-building toolkits, Cookoo maps URLs to routes. But unlike most other frameworks, it maps routes to a series of sequentially executed commands. That's why we call Cookoo a "chain of command" framework--it maps a route to a chain of commands.

We can create a very simple home page by mapping a route to just one command:

package main

import (
    "github.com/Masterminds/cookoo"
    "github.com/Masterminds/cookoo/web"
)

func main() {
    registry, router, context := cookoo.Cookoo()

    registry.Route("GET /", "The default home page").
        Does(web.Flush, "out").
        Using("content").WithDefault("Hello!")

    web.Serve(registry, router, context)
}

The important part is the new Route that we added:


    registry.Route("GET /", "The default home page").
        Does(web.Flush, "out").
        Using("content").WithDefault("Hello!")

To define our new route we've chained together a string of methods:

  • Route declares a new route. It takes a path (GET /) and a description (The default home page). It's made of two parts: The HTTP verb (GET, POST, PUT, etc.) and the URI path.
  • Within that route, we call Does() to tell it what command to execute. In this case, we execute web.Flush(), which just flushes data to an HTTP response. Every command in a chain has a name that you assign. We named ours out.
  • Finally, we specify what data we're passing to web.Flush with the Using() method. This tells Flush() that we are passing it a parameter named content, and we're setting content's default value to Hello!.

Given all of this, what should we expect if we restart the server and then point our browser at http://localhost:8080/? Let's test:

Hello!

We have a working web app! But it is sort of puny. Let's add support for Go's built-in template system.

Adding an HTML Template

Go comes with a built-in template engine. We can use that engine within Cookoo very easily.

Creating a Template

Let's do the easy part first. Let's create a simple HTML template named index.html. It's fine to have that file in the same directory as the server.go file.

<!DOCTYPE html>
<html>
  <head>
    <title>{{.Title}}</title>
  </head>
  <body>
    <h1>{{.Title}}</h1>
    {{.Content}}
  </body>
</html>

The above is just a minimal HTML5 document with a couple of template placeholders: {{.Title}} and {{.Content}}. These will expand to values we put into the Cookoo context in just a few moments.

Go templates are very powerful, but for our purposes the template above is a good illustration.

Now let's add template support to our web app.

Adding Templates

Cookoo passes data around using a cookoo.Context. A context is a general purpose data bus. Most of the time it is used only to pass data around within a particular route. But you can also add things to the global context and have the data added to every single route. (Just be careful to only add things that can handle concurrent access.)

Here we're going to add some templates to the context. We only need to do two things:

  1. We need to create a template bundle.
  2. then we need to add it to Cookoo as a datasource.

Here's the full source. Note that we've only added a couple of lines.

package main

import (
    "github.com/Masterminds/cookoo"
    "github.com/Masterminds/cookoo/web"

    "html/template"
)

func main() {
    registry, router, context := cookoo.Cookoo()

    tpl := template.Must(template.New("main").ParseGlob("*.html"))
    context.Add("tpl", tpl)


    registry.Route("GET /", "The default home page").
        Does(web.Flush, "out").
        Using("content").WithDefault("Hello!")

    web.Serve(registry, router, context)

}

Now we have a template engine added to our server. We just need to access it. Fortunately, Cookoo comes with built-in support. And as we do this, we can learn more about Cookoo's chaining.

A Chain of Commands

We can now re-write our initial route to do three things instead of one:

  1. Set a Title and Content so the template can fill in those values.
  2. Render the template we created before.
  3. Send the content back to the web browser.

Here's our new route:

    registry.Route("GET /", "The default home page").
        Does(cookoo.AddToContext, "_").
            Using("Title").WithDefault("Hello").
            Using("Content").WithDefault("Hi there!").
        Does(web.RenderHTML, "html").
            Using("template").From("cxt:tpl").
            Using("templateName").WithDefault("index.html").
        Does(web.Flush, "out").
            Using("content").From("cxt:html").
            Using("contentType").WithDefault("text/html")

(I've added some tabs to make it easier to read)

  • cookoo.AddToContext adds data directly to the context. We use it to put data where the template can access. it. Title will be available to the template as {{.Title}}.
  • web.RenderHTML is the template renderer. We have to pass it two things: The template renderer, which we added to the context as tpl. So we get it with From("cxt:tpl"). And it also needs the HTML template that we created.
  • Finally, we need to send all of this to the browser with web.Flush.

Importantly, we are now frequently using the context to pass data from one command to the next. Each time a command executes, its results are accessible through the context. A later command can then access that data using From("cxt:NAME"). (From() is very powerful, and can be used to access data from other places as well.) This is how the chain of command pattern works: As each command runs, it makes its results accessible to other commands via the context. Then commands can build upon each other.

The Result

We've seen how to build an app. Now let's restart the server with this new code. When we access http://localhost:8080, we'll see this output:

<!DOCTYPE html>
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <h1>Hello</h1>
    Hi there!
  </body>
</html>

The {{.Title}} sections have been filled in with the value we set in AddToContext, and so has the {{.Content}} placeholder.

Going Forward

This post explains how to create simple Web apps with Cookoo. But to get from here to something truly useful, you'll need to add more commands to your chains. Cookoo comes with many basic commands (including classes to efficiently access SQL databases), but most of the time you will write at least a few commands on your own.

In an upcomming post I'll introduce commands in more detail and show how easily they can be written.



comments powered by Disqus