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:
- github.com/Masterminds/cookoo: This contains the Cookoo core, and pretty much all Cookoo apps import this package.
- github.com/Masterminds/cookoo/web: This contains the web helpers for Cookoo, including the web server and a variety of generally useful commands (like
web.Flush
).
The main Cookoo parts
The first line of the main()
function calls cookoo.Cookoo()
to create three things:
- The registry
- The router
- The base context
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:
- Accessing data about its runtime environment
- Storing data that later commands may use
- Accessing long-term data services (think database connections) as
datasources
- Logging
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:
contentType
, which has a default value oftext/plain
content
, which has a default value of"Hello World"
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:
- The
cookoo.Context
, which holds data about the current route. - The
cookoo.Params
instance, which holds the parameters passed into this specific command (think of it likeargv
on steriods).
And a command returns two things:
- Some value, which may be
nil
. This will get stored in the context as the return value for the command. - A
cookoo.Interrupt
if you want to interrupt the current route. This can be one of several things, including...- An
error
,cookoo.FatalError
, orcookoo.RecoverableError
. The first two will halt the executio of the route and report the error. The third will report the error, but continue executing the route. - A
cookoo.Reroute
, which will begin executing a different route. - A
cookoo.Stop
, which will simply tell the current route to stop and return.
- An
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
- 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. - 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.