Tutorial: Building A Web Application

In this short tutorial we will learn how to build a simple Web server with Cookoo.

While you can build your own Cookoo-based web server, the project comes with all of the basics out of the box. Usually, that's all you need.

Let's start with a simple "Hello World" server:

package main

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

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

    registry.Route("GET /", "The Homepage").
        Does(web.Flush, "out").
        Using("contentType").WithDefault("text/plain").
        Using("content").WithDefault("Hello World")

    web.Serve(registry, router, cxt)
}

The above is a very simple (but functional) web server. It listens only for GET requests on the root (/) URL for this server. And it answers with a plain text document containing only the string Hello World.

Tip: If your routes will execute goroutines (as many do), you should synchronize the context before calling web.Serve(). For example: web.Serve(registry, router, cookoo.SyncContext(cxt))

Here's how it works.

The imports

Above, we import two packages:

The main Cookoo parts

The first line of the main() function calls cookoo.Cookoo() to create three things:

The registry is used to declare routes. With it, we map route patterns to the chain of commands that each route will execute. In our example, we use it to create one route.

The router is the piece responsible for executing the routes defined in the registry. In some Cookoo apps, you manually call router.HandleRequest(). But with our web server, we'll pass that responsibility on to web,Serve().

The context is the general-purpose container for execution data. As a route executes, commands use the context for four things:

In the example above, we don't directly use the context for anything. But behind the scenes, Cookoo is using it to manage information.

Building the registry

Here's our registry entry:

registry.Route("GET /", "The Homepage").
    Does(web.Flush, "out").
    Using("contentType").WithDefault("text/plain").
    Using("content").WithDefault("Hello World")

Just by reading it, we should be able to understand what it is doing:

When we receive a request for "GET /" (which we'll call "The Homepage"), the router Does (or executes) web.Flush (and we call that step "out") Using two different parameters:

Since we don't specify any way of overriding those defaults, they will always be the case.

So, for example, if we use Curl to access our site, we will get a Hello World response:

$ curl localhost:8080/
Hello World

And... the server

The last line of our main() function does the obvious:

web.Serve(registry, router, cxt)

This starts up a web server, and passes in our registry, router, and context. Under the hood, Cookoo handles the basics of web serving, including basic error handling (404 and 500 errors).

You can run the server on a UNIX-like system with this command:

$ go run server.go

And whenever you're tired of it, you can kill it with CTRL-c.

One Step Further

Now that we have the basics, let's write our own command and add it to the existing chain. For simplicity, this command will create a simple text string.

The Cookoo Command

Cookoo commands are just functions of the cookoo.Command type:

type Command func(cxt Context, params *Params) (interface{}, Interrupt)

So it's a function that takes two things:

And a command returns two things:

Don't fret over the details. The command we are about to build will show the basics, and the rest you will pick up as you go.

Making Our Own

Now we can write a simple command that meets that interface.

As with the rest of Go, you can organize source as you wish. I have created a new file, which I have named cmd.go. And here's what's in it:

package main

import (
    "github.com/Masterminds/cookoo"
    "time"
    "fmt"
)

// MyMessage builds a simple text message and put it in the context.
func MyMessage(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
    tpl := "Route %s executed on %s"
    now := time.Now().Format("Jan 2, 2006 at 3:04pm (MST)")
    name := c.Get("route.Name", "unknown").(string)
    msg := fmt.Sprintf(tpl, name, now)

    return msg, nil
}

Really, all we're doing here is creating a short string that says "Route NAME exectued on DATE". Nothing fancy. So let's take a look at how it works.

The first few lines of the function body are generic. We create a string format (mainly so the code above formats well), and we create a string indicating today's date/time.

But the third line of the function body illustrates one of the features of Cookoo:

    name := c.Get("route.Name", "unknown").(string)

Recall that c is a handle to the cookoo.Context. There are a number of data that Cookoo puts into the context for each run. One such piece of information is route.Name, which is the name of the currently executing route (e.g. GET /). We could also get the description of the route with route.Description.

Get() takes the name of a context value as the first param, and a default value as the second. Since context values are stored as interface{}, you will have to explicitly change the type. that's why we do .(string) at the end.

Tip Cookoo's Context and Params both follow a convention where there are both Get() and Has() methods. Has() does not take a default value, and returns interface{}, bool, where the bool indicates whether the value was found. (That, in turn, allows you to intentionally store nil or default values in the context.)

From there, we just format a string and return the resulting message. Note that the last line of the function does not return a cookoo.Interrupt.

Adding The Command

Now we can modify the route that we declared earlier in the article:

    registry.Route("GET /", "The Homepage").
        Does(MyMessage, "msg").
        Does(web.Flush, "out").
        Using("contentType").WithDefault("text/plain").
        Using("content").From("cxt:msg")

Notice that we've added Does(MyMessage, "msg"). That means that the first command to be executed during processing will be MyMessage, and that it's return value will be stored in the Context with the name msg.

So the formatted string is generated and then stored in the context. How do we use it?

Take a look at the second command. We've modified it since our first example:

        Does(web.Flush, "out").
        Using("contentType").WithDefault("text/plain").
        Using("content").From("cxt:msg") // <-- Ooo!

Instead of setting the content param to a default message, we tell it to get its value from cxt:msg, which means "Get msg from the context".

Two Tips

  1. You can combine .From().WithDefault() to achieve the effect of trying to get a value from the context, but using a default if nothing is found.
  2. You can draw from multiple sources in a single From() clause. In this case, it will search the sources in the given order: From("cxt:msg path:1 post:foo" would look for 'msg' in the context, the second URL path variable, or the 'foo' value in POSTed form data.

Now if we were to start the server (go run server.go cmd.go), we would get data like this:

$ curl localhost:8080/
Route GET / executed on Apr 22, 2014 at 8:45am (MDT)

At this point you should have a basic understanding of working with Cookoo as a web server. Check out some of the other articles here to learn more.