Creating a Cookoo Web App (in 23 Lines of Code)
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 withcookoo.Cookoo()
, and then we start a web server withweb.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 executeweb.Flush()
, which just flushes data to an HTTP response. Every command in a chain has a name that you assign. We named oursout
. - Finally, we specify what data we're passing to
web.Flush
with theUsing()
method. This tellsFlush()
that we are passing it a parameter namedcontent
, and we're settingcontent
's default value toHello!
.
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:
- We need to create a template bundle.
- 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:
- Set a
Title
andContent
so the template can fill in those values. - Render the template we created before.
- 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 astpl
. So we get it withFrom("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.