Web Application Anatomy: How do web apps work?

The author
Stephen Castle3 years ago

Web applications are everywhere. Every shop, blog, message board, and other web page you visit with a web browser is some type of web application. Because the web is so ubiquitous, this type of software is one of the most common types of software in the world. You can write web apps in many different ways and with many different languages, there are almost as many ways to write web apps as there are stars in the sky, however a few things are universally consistent. In this workshop I hope to break down a simple but real working web app to its constituent parts in a way that will help you understand the basic structure shared by all web applications.

What we are going to build

Project Overview We are going to build a real useable blog almost entirely from scratch. We will only use the Go programming language and 5 libraries, no large frameworks. This application will serve a web page where you can browse and read blog articles. It will also allow you to create blog posts, and edit existing blog posts which will be stored in a database.

The entire program will be just 412 lines of Go in a single file and 4 HTML templates. To see the completed project check out this Github Repo (Go Tiny Blog Source Code). Feel free to clone it and play around, then if you feel comfortable you can follow along as we build it back up to help reinforce the concepts. I recommend typing along as opposed to copy and pasting the code because it will help you slow down and think about what is happening and reinforce your understanding.

Topics we are going to learn about

Topics we are not going to learn about (in this workshop)

Anatomy of a Web Application

Before we start coding let's discuss some of the fundamental components shared by every web application.

The Web Server and Client

Requests and Responses Chart

Web apps are made up of two separate running computer programs, a server and a client. They are both important and the main distinction is whose machine they run on. The server runs on a machine controlled by you the web app creator, and the client runs on a machine controlled by the user of the web application. This could be their laptop, desktop computer or their mobile phone. It could be a program running in a web browser, or a Native Application running directly on their computer. Sometimes a web application has a single server that is shared by many different clients and sometimes a web application is made of multiple servers that all talk to each other before responding to a client. In our example we will focus on just a single server program and a single HTML client running in a web browser.

Our server will be a Go application, and a client will be an HTML web page running in a web browser. Interestingly note how the server can in fact serve its own client in this way.

Every web application communicates via a request and a response (although they can also communicate in a few other ways we won't be covering in this tutorial). These are messages shared back and forth between the server and the client. The client sends a request asking for information, and the server sends back a response after doing some processing on the request. The reason that web clients and web browsers know how to understand these messages is because they all are written in the same way.

The HTTP Standard

The Request and Response messages are just text, but how do the client and server know how to speak the same language? That problem is solved by the HTTP Standard. The HTTP standard is a set of rules for how the messages are allowed to be structured. Every web browser and web server is then written so that it always follows these rules. We won't attempt to learn everything that's in the standard but by looking at a typical request and response message we can understand the important and required parts which they contain.

The Request

The request is constructed by the client and sent to the server, it contains text formatted according to the HTTP standard that the web server will process and use to decide what to respond with.

It has two important parts. The headers, and an optional body. The headers are key value pairs that contain common general purpose information. The body can contain many different types of information which we will talk about later.

In our blog app this is what the browser sends to the server when it requests the home page of the blog.

GET / HTTP/1.1
Host: localhost:8000
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9

OPTIONAL CONTENT FOR THE REQUEST GOES HERE

We can mostly focus on just the first line which contains all the information we need to route a request, and also the body of the request which contains any special information the client is sending to the server.

Request URI or Path

GET /any-path/can-go-here HTTP/1.1

This part of the request along with the HTTP method are the two most important parts of the request. They tell the server what the client is requesting. URI stands for Uniform Resource Identifier and you can think of it as an address for some resource that the server has. This could be a file, some data, or a HTML web page. Its up to the server to decide.

Request Method

GET / HTTP/1.1

While the URI tells the server what Resource the request is interested in, the method tells the server what the request wants the server to do to it. There are nine valid methods defined by the HTTP protocol.

For more information on these methods the Mozilla docs have some great detailed information.

Mozilla HTTP request methods

Our blog web app will use 3 of the methods.

A Note on REST

You might have heard the term rest before when talking about Uniform Resource Indicators. REST is a set of rules and guidelines for how you should handle the routing of various combinations of HTTP methods and URIs For more information on REST check out this great blog post https://www.infoq.com/articles/rest-introduction/

HTTP Version

GET / HTTP/1.1

There is more than one version of the HTTP protocol. This part of the request header let's the server know which version of HTTP request the client is sending. Most of the time you don't need to worry about this.

The Request Body

The request can include an optional section called the body which comes after all of the headers and a single space. This can be formatted a number of different ways, but one of the most common is as a JSON string. This is what the BODY looks like in the request our sample app is going to make to create a new blog post.

It sends some JSON with the new title, author name, and blog post content. The server will be able to read this data and save the blog post to the database.

POST / HTTP/1.1
Host: localhost:8000
Connection: keep-alive
Content-Length: 100
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://localhost:8000
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: io=eTGK2Lng2938cmNfAAAB

{title: "Hello World", author: "Stephen Castle", body: "This is the content of the new blog post."}

Notice that when the Request has a body that there is also a Content-Type header which describes the format of the content. There are a few other types of content that are commonly used in post requests but we won't be using any of them to create our blog.

The Web Server

Once the client has created a Request message like the one above, it sends it to the server. When the server receives it the first thing it needs to do is parse it into an object it can understand. If the server can not understand the Request it will return a special error to the client to inform it that the request was malformed. If it can understand it the next step is to apply some rules to decide what to do with the Request, this process is called Routing.

Web Application Router

Request Routing

Request routing is all about matching any given Request to a handler function. To do this you write a list of rules called routes which can match patterns in the Request. The most common patterns you will match are the URI (Uniform Resource Identifier) combined with the HTTP method of the request.

Every web server and programming language has a different syntax for representing routes, but they all do the same thing. Match patterns in the requests. This is what the router will look like in our blog. Each time you call r.HandleFunc it creates a new route. Notice how the first argument to the function is a string. This string matches URIs in the request. The second argument is a function which should get called to handle the request.

Here are a few other notes on Routes.

  1. Routes can capture variables which will be provided to the handler. When you see the curly braces it will match anything and store it as a variable with the same name as the text inside the curly braces.
  2. The same URI can have different handlers for different HTTP methods. Notice how we will use a function called homeHandler to handle get requests to the home page, but a different method called createPostHandler for POST requests to that same path.
  3. Try not to worry too much about this specific syntax, its different in every language and web server. The important thing is matching a request to a function.

A real router for a Blog Application

func newRouter(db *bolt.DB) *mux.Router {
	r := mux.NewRouter()
	r.StrictSlash(true)
	r.HandleFunc("/", homeHandler(db, homeTemplate)).Methods("GET")
	r.HandleFunc("/", createPostHandler(db)).Methods("POST")
	r.HandleFunc("/create", createPostPageHandler(db, createTemplate)).Methods("GET")
	r.HandleFunc("/{slug}", getPostHandler(db, postTemplate)).Methods("GET")
	r.HandleFunc("/{slug}", modifyPostHandler(db)).Methods("POST")
	r.HandleFunc("/{slug}", deletePostHandler(db)).Methods("DELETE")
	r.HandleFunc("/{slug}/edit", editPostPageHandler(db, editTemplate)).Methods("GET")
	return r
}

This router setup creates 4 web page routes that return HTML pages. (You can't actually tell what they return without looking at the handler functions)

  1. The Home Page - A GET to / routes to a function named homeHandler
  2. The Post Detail Page - A GET to /anything-goes-here routes to a function named getPostHandler
  3. The Create Post Form Page - A GET to /create routes to a function named createPostPageHandler
  4. The Edit Post Page - A GET to /anything-goes-here/edit routes to a function named editPostPageHandler

And Three endpoints that take JSON as the Request body and make changes to the data.

  1. The Create Post Endpoint - A POST to / routes to a function named createPostHandler
  2. The Edit Post Endpoint - A POST to /anything-can-go-here routes to a function named modifyPostHandler
  3. The Delete Post Endpoint - A DELETE to /anything-can-go-here routes to a function named deletePostHandler

Request Handler Functions

Request Handlers

Once the web server has matched a request up to a function, the request gets routed along to that function. In Go a handler function must accept two arguments, a response writer, and a request object. The Go web server uses the http.ResponseWriter object to send the response back to the client by calling the WriteHeader and Write methods on it. This is specific to Go and could work very differently in other languages. The important thing is that this function gets the request and then SOMEHOW creates a response back to the client. The way it creates the response can be any way you can imagine. It can make a request to a different web server, do a calculation, or call out to a database to get some information. The sky is the limit. In our app all of the request handlers will connect to a database to get and modify the blog posts and then return them as either part of an HTML page or in some cases as JSON to be used by javascript.

This is a real handler that can access a list of posts from a database and returns a properly formatted Response with the ContentType set to html and a body containing HTML from a template.

// homeHandler returns the list of blog posts rendered in an HTML template.
func homeHandler(db *bolt.DB, t *template.Template) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		postData, err := listPosts(db)
		if err != nil {
			res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
			res.WriteHeader(http.StatusInternalServerError)
			res.Write([]byte("Could not list posts."))
			return
		}
		log.Println("Requested the home page.")
		res.Header().Set("Content-Type", "text/html; charset=UTF-8")
		res.WriteHeader(http.StatusOK)
		t.Execute(res, HomePageData{SiteMetaData: siteMetaData, Posts: postData})
	}

	return fn
}

The response object sent back by our blog home page.

Running the function above will result in the following being sent back to the client as the response.

Just like the request object it starts with headers, then a space, then the content of the response. The Content-Type header tells the client what type of content was sent back so it can process it correctly. In this case it is the HTML webpage representing our home page. The browser will know how to take this content and display it on the screen.

The Response header also contains a Status Code which tells the client if the request was successful, or if there was some type of error. For a complete list of status codes take a look at this link.

HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 time: Mon, 30 Nov 2020
02:40:50 GMT Content-Length: 1425

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Go Tiny Blog</title>

    <style>
      body {
        font-family: arial;
        margin: 0.4rem;
      }
      main {
        display: flex;
        flex-direction: column;
        max-width: 600px;
        margin: auto;
      }
      h1 {
        font-size: 3rem;
      }
      h2 {
        font-size: 1.5rem;
        margin-top: 2rem;
      }
      p {
        font-size: 1rem;
      }
      ul {
        list-style: none;
        margin-top: 1rem;
        padding: 0;
      }
      li {
        margin-top: 0.5rem;
      }
      a {
        font-weight: 600;
        color: #ff4f98;
        text-decoration: none;
      }
      a:hover {
        color: #ff529a;
        text-decoration: none;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Go Tiny Blog</h1>
      <a href="/create">Write a new post</a>
      <p>
        A one file, simple to reason about blog designed to demonstrate the
        basic anatomy of a web app.
      </p>
      <h2>Recent Posts</h2>
      <ul>
        <li>
          <a href="2020-11-29t15-05-42-08-00-hello-world"> Hello World</a>
        </li>

        <li>
          <a href="2020-11-29t15-06-07-08-00-hello-world"> Hello World</a>
        </li>
      </ul>
    </main>
  </body>
</html>

Handlers can return things other than HTML, sometimes they just return data for the web application to use. In our Blog we will use this type of route to send JSON and get back JSON if the request to create or edit a blog post is successful. Here is an example of such a route.

This route is a lot more complicated because it needs to understand the JSON sent in the Request body and turn it into a post object the system can understand. Once it does that it sets the date the object was posted, and creates a slug for it by using the date and the title of the blog post. Then it calls a function to save the new post to the database. If that operation is successful it returns the newly created object, if it fails it returns an error.

// createPostHandler handles posted JSON data representing a new post, and stores it in the database.
// It creates a slug to use as a key using the title of the post.
// This implies in the current state of affairs that titles must be unique or the keys will overwrite each other.
func createPostHandler(db *bolt.DB) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		var post Post
		res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
		// Reads in the body content from the post request safely limiting to max size.
		body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
		if err != nil {
			panic(err)
		}
		// Close the Reader.
		if err := r.Body.Close(); err != nil {
			panic(err)
		}

		// Convert the JSON to a Post struct and write it to the post variable created at the top
		// of the handler.
		if err := json.Unmarshal(body, &post); err != nil {
			res.Header().Set("Content-Type", "application/json; charset=UTF-8")
			res.WriteHeader(422) // unprocessable entity
			if err := json.NewEncoder(res).Encode(err); err != nil {
				panic(err)
			}
		}

		// Set the creation time stamp to the current server time.
		post.DatePosted = time.Now()

		// Create a URL safe slug from the timestamp and the title.
		autoSlug := fmt.Sprintf("%s-%s", slug.Make(post.DatePosted.Format(time.RFC3339)), slug.Make(post.Title))
		post.Slug = autoSlug

		if err = upsertPost(db, post, autoSlug); err != nil {
			res.WriteHeader(http.StatusInternalServerError)
			res.Write([]byte("Error writing to DB."))
			return
		}

		res.Header().Set("Content-Type", "application/json; charset=UTF-8")
		res.WriteHeader(http.StatusCreated)
		if err := json.NewEncoder(res).Encode(post); err != nil {
			panic(err)
		}
	}
	return fn
}

The response object for the createPost end point.

Running the function above will result in the following being sent back to the client as the response if it succeeds.

Notice how this time instead of HTML in the body it is a JSON object, and the Content-Type header says text/json.

Building the Example Web Application From Scratch

We've been talking about the building blocks of a web app using a blog as an example. Let's actually write a real working version of that blog. We will start out by writing a small web server using Go. The reason for using Go in this demonstration is that it has a very straightforward built in library for creating servers that is widely used and very robust. It will also allow us to create our entire application in just one go file, and 4 HTML template files which serve as the client portion of the web app. When writing web applications in other languages you often need many more libraries and frameworks to achieve the same thing.

If you find the Go code difficult to follow that's ok, think of this blog you are creating as a toy to help you explore the concepts discussed above. No matter what language you choose to use in the future, these general ideas will continue to be applicable.

To reduce the chances you will get lost if you are new to Go, each code block below is an iteration of the entire program. Meaning at any point you can replace the entire contents of main.go with one of the code blocks and it will run and be at that step in the tutorial. To better understand each piece of the large code blocks there are more detailed comments included at each step along the way.

Step 1 Install GO

First you will need to have the GO Programming Language installed in order to write the program that will act as the web server. For help installing GO you can refer to this link below.

How to Install Go

Step 2 Create a Go Project

To initialize a new go project first create a new directory somewhere on your computer. You can call it go-tiny-blog if you want. Then open a terminal inside of that directory and run the following command.

go mod init github.com/codeworkshop-dev/go-tiny-blog

this will initialize a new go project in the directory and you should see a new file appear called go.mod

Next you can install all of the GO packages we will be using for this project. There are 5.

to install them run

go get github.com/boltdb/bolt && go get github.com/gomarkdown/markdown&& go get github.com/gorilla/mux && go get github.com/gosimple/slug && go get github.com/microcosm-cc/bluemonday

Create a file in your go-tiny-blog directory called main.go and fill in the below code snippet.

package main

func main(){
	fmt.Println("Hello World")
}

This will create the simplest possible runnable Go program. To run it type in your terminal while located in the go-tiny-blog directory this command go run . That will build the program and immediately run it for you. If it worked you should see Hello World printed to your terminal.

Step 3 Write a Hello World Web Server in Go

Now let's create an actual web server. In the same main.go file created above replace all of the code with the code below. This will create a web server with one route, and one handler that returns the words "Home Page!".

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

	"github.com/gorilla/mux"
)

func homeHandler(res http.ResponseWriter, r *http.Request) {
	res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
	res.WriteHeader(http.StatusOK)
	data := []byte("Home Page!")
	res.Write(data)
}

func main() {
	r := newRouter()
	// Create http server and run inside go routine for graceful shutdown.
	srv := &http.Server{
		Handler:      r,
		Addr:         "127.0.0.1:8000",
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	log.Println("Starting up..")


	// The following code exists just to make the web server shutdown gracefully.
	// Try not to worry about it too much because the scope goes beyond what we
	// are trying to learn
	go func() {
		if err := srv.ListenAndServe(); err != nil {
			log.Println(err)
		}
	}()

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	<-c
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
	defer cancel()

	srv.Shutdown(ctx)
	log.Println("Shutting down..")
	os.Exit(0)
}


// This is where we define the router for the web application.
// For now it only has one route to serve the home page.
func newRouter() *mux.Router {
	r := mux.NewRouter()
	r.HandleFunc("/", homeHandler)
	return r
}

To run this file from the terminal still located in your go-tiny-blog directory run the command

go run .

This will start up the server. To test it open your web browser and visit http://localhost:8000

You should see the words "Home Page!" Congratulations you just created a web application! This satisfies all the requirements we set out for being a web app. It can receive a response, route it, and return a response based on the contents of the request. Next let's start adding on bits and pieces to make this simple program in to a real blog.

Step 4 Draw the rest of the Blog

Ok so this is a big step! Don't worry if it seems like a lot is happening. I don't want you to worry too much about the go syntax, just to get an experience writing a program that is a real fuctioning web app. Feel free to just copy and paste the entire files in to place if you want and go back and study them later.

In this step we will accomplish the following things.

In the last step we had one route that returned the text "Home Page!" Now we want to start building something real so we would prefer to return HTML pages.

To do that in GO we can use the html/template package to load in and template html based on dynamic data.

The Home Page

We are going to need an HTML page to list our blog posts on the home page. In Go you do this by creating HTML templates. The templates are html with an extended templating language. Anywhere you see the double curly brackets (like this {{ }}) we are templating in data.

For more information on Go HTML templates refer here. Go Templating Language Help

Create a folder in the same directory as your main.go file called templates. And then inside that directory create a file called home.html and give it the following contents.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{{.SiteMetaData.Title}}</title>

    <style>
      body {
        font-family: arial;
        margin: 0.4rem;
      }
      main {
        display: flex;
        flex-direction: column;
        max-width: 600px;
        margin: auto;
      }
      h1 {
        font-size: 3rem;
      }
      h2 {
        font-size: 1.5rem;
        margin-top: 2rem;
      }
      p {
        font-size: 1rem;
      }
      ul {
        list-style: none;
        margin-top: 1rem;
        padding: 0;
      }
      li {
        margin-top: 0.5rem;
      }
      a {
        font-weight: 600;
        color: #ff4f98;
        text-decoration: none;
      }
      a:hover {
        color: #ff529a;
        text-decoration: none;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>{{.SiteMetaData.Title}}</h1>
      <a href="/create">Write a new post</a>
      <p>{{.SiteMetaData.Description}}</p>
      <h2>Recent Posts</h2>
      <ul>
        {{ range $key, $value := .Posts }}
        <li><a href="{{ $key }}"> {{ $value.Title }}</a></li>
        {{ end }}
      </ul>
    </main>
  </body>
</html>

The Post Detail Page

We also need an HTML page to display the post details when you are reading a specific post.

Create another file called post.html inside the templates directory with the following contents.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{{.Post.Title}} - {{.SiteMetaData.Title}}</title>
    <style>
      body {
        font-family: arial;
        margin: 0.4rem;
      }
      main,
      header,
      footer {
        display: flex;
        flex-direction: column;
        max-width: 600px;
        margin: auto;
      }
      h1 {
        font-size: 3rem;
      }
      h2 {
        font-size: 1.5rem;
      }
      p {
        font-size: 1rem;
        line-height: 1.6rem;
      }
      img {
        display: block;
        width: 100%;
      }

      a {
        font-weight: 600;
        color: #ff4f98;
        text-decoration: none;
      }
      a:hover {
        color: #ff529a;
        text-decoration: none;
      }
    </style>
  </head>
  <body>
    <header>
      <h1>{{.Post.Title}}</h1>
      <a href="/{{.Post.Slug}}/edit">Edit post</a>
      <span><strong>By: </strong>{{.Post.Author}}</span>
      <span><strong>Published At: </strong>{{.Post.DatePosted}}</span>
    </header>
    <main>{{.HTML}}</main>
    <footer>
      <a href="/">Back</a>
    </footer>
  </body>
</html>

main.go

This will be our new main.go file. This completely replaces the main.go from the last step. It introduces structs to store blog post data, and templates to render that data out as an HTML page.

Most importantly it finally adds our second route to the router. So now we have two pages, a home page, and a blog post detail page with a slug variable.

The handler for the blog post detail page can use that slug handler to look up the blog post content in the Map defined under the SAMPLE DATA comment.

package main

import (
	"context"
	"html/template"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

	"github.com/gorilla/mux"
)

// SiteMetaData is a struct storing general information about the Site
type SiteMetaData struct {
	Title       string
	Description string
}

// Post is a struct defining the data structure for a blog post.
type Post struct {
	Author     string
	Body       string
	DatePosted time.Time
	Slug       string
	Title      string
}

// PostMap is a map of posts with the slug as the key.
type PostMap map[string]Post

// Test Data
// We can make our web app display posts directly from hardcoded data in memory.
// No need for a database... yet.

var siteMetaData = SiteMetaData{
	Title:       "Go Tiny Blog",
	Description: "A simple to reason about blog demonstrating the anatomy of a web app.",
}

var posts = PostMap{
	"hello-world": {
		Author:     "Stephen",
		Body:       "The world welcomes you.",
		Title:      "Hello World",
		Slug:       "hello-world",
		DatePosted: time.Now(),
	}, "hello-mars": {
		Author:     "Stephen",
		Body:       "Mars welcomes you.",
		Title:      "Hello Mars",
		DatePosted: time.Now(),
	}, "hello-venus": {
		Author:     "Stephen",
		Body:       "Venus welcomes you.",
		Title:      "Hello Venus",
		DatePosted: time.Now(),
	},
}

// HomePageData is the data required to render the HTML template for the home page.
type HomePageData struct {
	SiteMetaData SiteMetaData
	Posts        PostMap
}

// PostPageData is the data required to render the HTML template for the post page.
type PostPageData struct {
	SiteMetaData SiteMetaData
	Post         Post
}

func main() {
	r := newRouter()
	// Create http server and run inside go routine for graceful shutdown.
	srv := &http.Server{
		Handler:      r,
		Addr:         "127.0.0.1:8000",
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	log.Println("Starting up..")
	go func() {
		if err := srv.ListenAndServe(); err != nil {
			log.Println(err)
		}
	}()

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	<-c
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
	defer cancel()

	srv.Shutdown(ctx)
	log.Println("Shutting down..")
	os.Exit(0)
}

// In Go you need to load the templates like this before you can use them.
// For now they are loaded into global memory, but later we will refactor
// to load in to routes through dependency injection.
var homeTemplate = template.Must(template.ParseFiles("templates/home.html"))
var postTemplate = template.Must(template.ParseFiles("templates/post.html"))

// homeHandler returns the list of blog posts rendered in an HTML template.
func homeHandler(res http.ResponseWriter, r *http.Request) {
	log.Println("Requested the home page.")
	res.Header().Set("Content-Type", "text/html; charset=UTF-8")
	res.WriteHeader(http.StatusOK)
	homeTemplate.Execute(res, HomePageData{SiteMetaData: siteMetaData, Posts: posts})
}

// postHandler looks up a specific blog post and returns it as HTML.
func postHandler(res http.ResponseWriter, r *http.Request) {
	// Get the URL param named slug from the response.
	slug := mux.Vars(r)["slug"]
	post, ok := posts[slug]
	if !ok {
		res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
		res.WriteHeader(http.StatusNotFound)
		res.Write([]byte("404 Page Not Found"))
		return
	}
	log.Println("Requested post.")
	res.Header().Set("Content-Type", "text/html; charset=UTF-8")
	res.WriteHeader(http.StatusOK)
	postTemplate.Execute(res, PostPageData{SiteMetaData: siteMetaData, Post: post})
}

// newRouter configures and sets up the gorilla mux router paths.
// Now there are 2 routes. The postHandler route is our first route with a
// URL parameter which is called slug. The slug is the key we will use to look up a specific blog post.
func newRouter() *mux.Router {
	r := mux.NewRouter()
	r.StrictSlash(true)
	r.HandleFunc("/", homeHandler).Methods("GET")
	r.HandleFunc("/{slug}", postHandler).Methods("GET")
	return r
}

After getting these files updated, try running your Blog. You can run it by executing the command go run . in the same directory as the main.go file. Visit http://lolcalhost:8000 to see the blog. Can you figure out how to change a blog post right now? Remember if you make any changes to the code you will need to stop the server and restart it again with the go run command.

Step 5 Add Persistence and a Handler For Creating Posts

In this step we are going to add a database to store posts. This will replace the hard coded posts we used in the previous step.

To do this we are going to add a database called BoltDB. BoltDB is a Key value store and it's also an embeded database. This does not come without downsides but it is also extremely easy to use. We just include it in our go binary and access posts by using their slug as the key.

main.go

Make the following changes to main.go to add the Database and some functions for fetching and modifying database content.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

	"github.com/boltdb/bolt"
	"github.com/gorilla/mux"
	"github.com/gosimple/slug"
)

// SiteMetaData is general information about the Site
type SiteMetaData struct {
	Title       string
	Description string
}

// Post is a struct defining the data structure for a blog post.
type Post struct {
	Author     string    `json:"author,omitempty"`
	Body       string    `json:"body,omitempty"`
	DatePosted time.Time `json:"datePosted,omitempty"`
	Title      string    `json:"title,omitempty"`
}

// PostMap is a map of posts with the slug as the key.
type PostMap map[string]Post


var siteMetaData = SiteMetaData{
	Title:       "Go Tiny Blog",
	Description: "A simple to reason about blog demonstrating the anatomy of a web app.",
}

// HomePageData is the data required to render the HTML template for the home page.
// It is made up of the site meta data, and a map of all of the posts.
type HomePageData struct {
	SiteMetaData SiteMetaData
	Posts        PostMap
}

// PostPageData is the data required to render the HTML template for the post page.
// It is made up of the site meta data, and a Post struct.
type PostPageData struct {
	SiteMetaData SiteMetaData
	Post         Post
}

func main() {

	db, err := setupDB()
	defer db.Close()

	if err != nil {
		log.Println(err)
	}

	r := newRouter(db)
	// Create http server and run inside go routine for graceful shutdown.
	srv := &http.Server{
		Handler:      r,
		Addr:         "127.0.0.1:8000",
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	log.Println("Starting up..")

	// This code is all about gracefully shutting down the web server.
	// This allows the server to resolve any pending requests before shutting down.
	// This works by running the web server in a go routine.
	// The main function then continues and blocks waiting for a kill signal from the os.
	// It intercepts the kill signal, shuts down the server by calling the Shutdown method.
	// Then exits when that is done.
	go func() {
		if err := srv.ListenAndServe(); err != nil {
			log.Println(err)
		}
	}()

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	<-c
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
	defer cancel()

	srv.Shutdown(ctx)
	log.Println("Shutting down..")
	os.Exit(0)
}

// Load and parse the html templates to be used.
var homeTemplate = template.Must(template.ParseFiles("templates/home.html"))
var postTemplate = template.Must(template.ParseFiles("templates/post.html"))

// homeHandler returns the list of blog posts rendered in an HTML template.
func homeHandler(db *bolt.DB) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		postData, err := listPosts(db)
		if err != nil {
			res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
			res.WriteHeader(http.StatusInternalServerError)
			res.Write([]byte("Could not list posts."))
			return
		}
		log.Println("Requested the home page.")
		res.Header().Set("Content-Type", "text/html; charset=UTF-8")
		res.WriteHeader(http.StatusOK)
		homeTemplate.Execute(res, HomePageData{SiteMetaData: siteMetaData, Posts: postData})
	}

	return fn
}

// postHandler looks up a specific blog post and returns it as an HTML template.
func getPostHandler(db *bolt.DB) http.HandlerFunc {

	fn := func(res http.ResponseWriter, r *http.Request) {
		// Get the URL param named slug from the response.
		slug := mux.Vars(r)["slug"]
		post, err := getPost(db, slug)
		if err != nil {
			res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
			res.WriteHeader(http.StatusNotFound)
			res.Write([]byte("404 Page Not Found"))
			return
		}
		log.Println("Requested post.")
		res.Header().Set("Content-Type", "text/html; charset=UTF-8")
		res.WriteHeader(http.StatusOK)
		postTemplate.Execute(res, PostPageData{SiteMetaData: siteMetaData, Post: *post})
	}
	return fn
}

// createPostHandler handles posted JSON data representing a new post, and stores it in the database.
// It creates a slug to use as a key using the title of the post.
// This implies in the current state of affairs that titles must be unique or the keys will overwrite each other.
func createPostHandler(db *bolt.DB) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		var post Post
		res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
		body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
		if err != nil {
			panic(err)
		}
		if err := r.Body.Close(); err != nil {
			panic(err)
		}
		if err := json.Unmarshal(body, &post); err != nil {
			res.Header().Set("Content-Type", "application/json; charset=UTF-8")
			res.WriteHeader(422) // unprocessable entity
			if err := json.NewEncoder(res).Encode(err); err != nil {
				panic(err)
			}
		}
		err = addPost(db, post, slug.Make(post.Title))
		if err != nil {
			res.WriteHeader(http.StatusInternalServerError)
			res.Write([]byte("Error writing to DB."))
			return
		}
		res.Header().Set("Content-Type", "application/json; charset=UTF-8")
		res.WriteHeader(http.StatusCreated)
		if err := json.NewEncoder(res).Encode(post); err != nil {
			panic(err)
		}
	}
	return fn
}

// DATA STORE FUNCTIONS

// addPost writes a post to the boltDB KV store using the slug as a key, and a serialized post struct as the value.
func addPost(db *bolt.DB, post Post, slug string) error {

	// Marshal post struct into bytes which can be written to Bolt.
	buf, err := json.Marshal(post)
	if err != nil {
		return err
	}

	err = db.Update(func(tx *bolt.Tx) error {
		err := tx.Bucket([]byte("BLOG")).Bucket([]byte("POSTS")).Put([]byte(slug), []byte(buf))
		if err != nil {
			return fmt.Errorf("could not insert content: %v", err)
		}
		return nil
	})
	return err
}

// listPosts returns a map of posts indexed by the slug.
func listPosts(db *bolt.DB) (PostMap, error) {
	results := PostMap{}
	err := db.View(func(tx *bolt.Tx) error {
		// Assume bucket exists and has keys
		b := tx.Bucket([]byte("BLOG")).Bucket([]byte("POSTS"))

		c := b.Cursor()

		for k, v := c.First(); k != nil; k, v = c.Next() {
			post := Post{}
			if err := json.Unmarshal(v, &post); err != nil {
				panic(err)
			}
			results[string(k)] = post
		}

		return nil
	})
	if err != nil {
		return nil, err
	}
	return results, nil
}

// getPost gets a specific post from the database by the slug.
func getPost(db *bolt.DB, slug string) (*Post, error) {
	result := Post{}
	err := db.View(func(tx *bolt.Tx) error {
		// Assume bucket exists and has keys
		b := tx.Bucket([]byte("BLOG")).Bucket([]byte("POSTS"))
		v := b.Get([]byte(slug))
		if err := json.Unmarshal(v, &result); err != nil {
			panic(err)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// INITIALIZATION FUNCTIONS
// setupDB sets up the database when the program start.
//  First it connects to the database, then it creates the buckets required to run the app if they do not exist.
func setupDB() (*bolt.DB, error) {
	db, err := bolt.Open("tinyblog.db", 0600, nil)
	if err != nil {
		return nil, fmt.Errorf("could not open db, %v", err)
	}
	err = db.Update(func(tx *bolt.Tx) error {
		root, err := tx.CreateBucketIfNotExists([]byte("BLOG"))
		if err != nil {
			return fmt.Errorf("could not create root bucket: %v", err)
		}
		_, err = root.CreateBucketIfNotExists([]byte("POSTS"))
		if err != nil {
			return fmt.Errorf("could not create post bucket: %v", err)
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("could not set up buckets, %v", err)
	}
	fmt.Println("DB Setup Done")
	return db, nil
}

// newRouter configures and sets up the gorilla mux router paths and connects the route to the handler function.
func newRouter(db *bolt.DB) *mux.Router {
	r := mux.NewRouter()
	r.StrictSlash(true)
	r.HandleFunc("/", homeHandler(db)).Methods("GET")
	r.HandleFunc("/", createPostHandler(db)).Methods("POST")
	r.HandleFunc("/{slug}", getPostHandler(db)).Methods("GET")
	return r
}

Step 6 Add Support for Markdown

We want our blog posts to look a little nicer than just big walls of text. Ideally our users could create blog posts with images, lists, and subheaders. Luckily, there we can use markdown to make all of that possible. Markdown is a simplified language for representing article style text formatting.

It looks like this.

## Header

- list item
- list item 2
  [Link Text]http://link.com

We can use a library called gomarkdown to take text like this as input and convert it to nice looking html elements in the blog post section of our template.

main.go

Make the following changes to main.go to add gomarkdown support. This step also adds a library called bluemonday to santize the markdown for dangerous HTML content that could hijack the page.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

	"github.com/boltdb/bolt"
	"github.com/gomarkdown/markdown"
	"github.com/gorilla/mux"
	"github.com/gosimple/slug"
	"github.com/microcosm-cc/bluemonday"
)

// SiteMetaData is general information about the Site
type SiteMetaData struct {
	Title       string
	Description string
}

// Post is a struct defining the data structure for a blog post.
type Post struct {
	Author     string    `json:"author,omitempty"`
	Body       string    `json:"body,omitempty"`
	DatePosted time.Time `json:"datePosted,omitempty"`
	Title      string    `json:"title,omitempty"`
	Slug       string    `json:"slug,omitempty"`
}

// PostMap is a map of posts with the slug as the key.
type PostMap map[string]Post

var siteMetaData = SiteMetaData{
	Title:       "Go Tiny Blog",
	Description: "A simple to reason about blog demonstrating the anatomy of a web app.",
}

// HomePageData is the data required to render the HTML template for the home page.
// It is made up of the site meta data, and a map of all of the posts.
type HomePageData struct {
	SiteMetaData SiteMetaData
	Posts        PostMap
}

// PostPageData is the data required to render the HTML template for the post page.
// It is made up of the site meta data, and a Post struct.
type PostPageData struct {
	SiteMetaData SiteMetaData
	Post         Post
	HTML         template.HTML
}

func main() {

	db, err := setupDB()
	defer db.Close()

	if err != nil {
		log.Println(err)
	}

	r := newRouter(db)
	// Create http server and run inside go routine for graceful shutdown.
	srv := &http.Server{
		Handler:      r,
		Addr:         "127.0.0.1:8000",
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	log.Println("Starting up..")

	// This code is all about gracefully shutting down the web server.
	// This allows the server to resolve any pending requests before shutting down.
	// This works by running the web server in a go routine.
	// The main function then continues and blocks waiting for a kill signal from the os.
	// It intercepts the kill signal, shuts down the server by calling the Shutdown method.
	// Then exits when that is done.
	go func() {
		if err := srv.ListenAndServe(); err != nil {
			log.Println(err)
		}
	}()

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	<-c
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
	defer cancel()

	srv.Shutdown(ctx)
	log.Println("Shutting down..")
	os.Exit(0)
}

// Load and parse the html templates to be used.
var homeTemplate = template.Must(template.ParseFiles("templates/home.html"))
var postTemplate = template.Must(template.ParseFiles("templates/post.html"))

// homeHandler returns the list of blog posts rendered in an HTML template.
func homeHandler(db *bolt.DB) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		postData, err := listPosts(db)
		if err != nil {
			res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
			res.WriteHeader(http.StatusInternalServerError)
			res.Write([]byte("Could not list posts."))
			return
		}
		log.Println("Requested the home page.")
		res.Header().Set("Content-Type", "text/html; charset=UTF-8")
		res.WriteHeader(http.StatusOK)
		homeTemplate.Execute(res, HomePageData{SiteMetaData: siteMetaData, Posts: postData})
	}

	return fn
}

// postHandler looks up a specific blog post and returns it as an HTML template.
func getPostHandler(db *bolt.DB) http.HandlerFunc {

	fn := func(res http.ResponseWriter, r *http.Request) {
		// Get the URL param named slug from the response.
		slug := mux.Vars(r)["slug"]
		post, err := getPost(db, slug)
		if err != nil {
			res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
			res.WriteHeader(http.StatusNotFound)
			res.Write([]byte("404 Page Not Found"))
			return
		}
		log.Printf("Requested: %s by %s \n", post.Title, post.Author)
		unsafePostHTML := markdown.ToHTML([]byte(post.Body), nil, nil)
		postHTML := bluemonday.UGCPolicy().SanitizeBytes(unsafePostHTML)
		res.Header().Set("Content-Type", "text/html; charset=UTF-8")
		res.WriteHeader(http.StatusOK)
		postTemplate.Execute(res, PostPageData{SiteMetaData: siteMetaData, Post: *post, HTML: template.HTML(postHTML)})
	}
	return fn
}

// createPostHandler handles posted JSON data representing a new post, and stores it in the database.
// It creates a slug to use as a key using the title of the post.
// This implies in the current state of affairs that titles must be unique or the keys will overwrite each other.
func createPostHandler(db *bolt.DB) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		var post Post
		res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
		body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
		if err != nil {
			panic(err)
		}
		if err := r.Body.Close(); err != nil {
			panic(err)
		}
		if err := json.Unmarshal(body, &post); err != nil {
			res.Header().Set("Content-Type", "application/json; charset=UTF-8")
			res.WriteHeader(422) // unprocessable entity
			if err := json.NewEncoder(res).Encode(err); err != nil {
				panic(err)
			}
		}
		autoSlug := fmt.Sprintf("%s-%s", slug.Make(post.DatePosted.Format(time.RFC3339)), slug.Make(post.Title))
		err = addPost(db, post, autoSlug)
		if err != nil {
			res.WriteHeader(http.StatusInternalServerError)
			res.Write([]byte("Error writing to DB."))
			return
		}
		res.Header().Set("Content-Type", "application/json; charset=UTF-8")
		res.WriteHeader(http.StatusCreated)
		if err := json.NewEncoder(res).Encode(post); err != nil {
			panic(err)
		}
	}
	return fn
}

// DATA STORE FUNCTIONS

// addPost writes a post to the boltDB KV store using the slug as a key, and a serialized post struct as the value.
func addPost(db *bolt.DB, post Post, slug string) error {

	// Marshal post struct into bytes which can be written to Bolt.
	buf, err := json.Marshal(post)
	if err != nil {
		return err
	}

	err = db.Update(func(tx *bolt.Tx) error {
		err := tx.Bucket([]byte("BLOG")).Bucket([]byte("POSTS")).Put([]byte(slug), []byte(buf))
		if err != nil {
			return fmt.Errorf("could not insert content: %v", err)
		}
		return nil
	})
	return err
}

// listPosts returns a map of posts indexed by the slug.
func listPosts(db *bolt.DB) (PostMap, error) {
	results := PostMap{}
	err := db.View(func(tx *bolt.Tx) error {
		// Assume bucket exists and has keys
		b := tx.Bucket([]byte("BLOG")).Bucket([]byte("POSTS"))

		c := b.Cursor()

		for k, v := c.First(); k != nil; k, v = c.Next() {
			post := Post{}
			if err := json.Unmarshal(v, &post); err != nil {
				panic(err)
			}
			results[string(k)] = post
		}

		return nil
	})
	if err != nil {
		return nil, err
	}
	return results, nil
}

// getPost gets a specific post from the database by the slug.
func getPost(db *bolt.DB, slug string) (*Post, error) {
	result := Post{}
	err := db.View(func(tx *bolt.Tx) error {
		// Assume bucket exists and has keys
		b := tx.Bucket([]byte("BLOG")).Bucket([]byte("POSTS"))
		v := b.Get([]byte(slug))
		if err := json.Unmarshal(v, &result); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// INITIALIZATION FUNCTIONS
// setupDB sets up the database when the program start.
//  First it connects to the database, then it creates the buckets required to run the app if they do not exist.
func setupDB() (*bolt.DB, error) {
	db, err := bolt.Open("tinyblog.db", 0600, nil)
	if err != nil {
		return nil, fmt.Errorf("could not open db, %v", err)
	}
	err = db.Update(func(tx *bolt.Tx) error {
		root, err := tx.CreateBucketIfNotExists([]byte("BLOG"))
		if err != nil {
			return fmt.Errorf("could not create root bucket: %v", err)
		}
		_, err = root.CreateBucketIfNotExists([]byte("POSTS"))
		if err != nil {
			return fmt.Errorf("could not create post bucket: %v", err)
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("could not set up buckets, %v", err)
	}
	fmt.Println("DB Setup Done")
	return db, nil
}

// newRouter configures and sets up the gorilla mux router paths and connects the route to the handler function.
func newRouter(db *bolt.DB) *mux.Router {
	r := mux.NewRouter()
	r.StrictSlash(true)
	r.HandleFunc("/", homeHandler(db)).Methods("GET")
	r.HandleFunc("/", createPostHandler(db)).Methods("POST")
	r.HandleFunc("/{slug}", getPostHandler(db)).Methods("GET")
	return r
}

Step 7 Create Client Side Pages for Adding and Editing Posts

Finally it's time to make our blog editable by adding routes for creating and editing blog posts. Not only do we need two new HTML pages with forms for editing content. We also need a new type of handler that works with JSON in the request.

These routes will handle POST requests and turn JSON into new blog posts in the database.

First we need two more templates. These will contain forms for creating posts and editing posts. They also contain some javascript to submit the form data to the server as a POST request.

Create Post Template

Time to create another html template. This time create a file called create-post.html in the same templates directory as before and copy the following code in to the file.

This template is different from the previous two because it has javascript. Javascript is a language that runs in the browser and let's us create more complex functionality for our web client. The javascript here is used to listen for when the user clicks the submit button. When that happens it grabs the value from all of the form fields, and sends them to the server with a normal HTTP request just like all the ones we did before. This time it is a POST request since we hope to create a new blog post using the data being sent.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Write a new post</title>
    <style>
      html,
      body,
      main {
        height: 100%;
      }
      body {
        background-color: lightgray;
        font-family: arial;
        margin: 0.2rem;
      }
      main,
      header {
        display: flex;
        flex-direction: column;
        max-width: 600px;
        margin: auto;
      }
      h1 {
        font-size: 3rem;
      }
      h2 {
        font-size: 1.5rem;
      }
      p {
        font-size: 1rem;
      }
      ul {
        list-style: none;
        padding: 0;
      }
      a {
        font-weight: 600;
        color: #ff4f98;
        text-decoration: none;
      }
      a:hover {
        color: #ff529a;
        text-decoration: none;
      }
      form {
        display: flex;
        flex-direction: column;
      }
      input,
      textarea,
      button {
        margin: 0.5rem 0;
        border-radius: 4px;
        border: none;
        padding: 12px;
      }
      textarea {
        flex: 1 1 0;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Write a new Post</h1>
      <a href="/">Back</a>
      <input name="title" id="title" placeholder="Title" />
      <input name="author" id="author" placeholder="Author" />
      <textarea name="body" id="body"></textarea>
      <button id="submit">Submit</button>
      <script>
        async function postData(url = "", data = {}) {
          const response = await fetch(url, {
            method: "POST",
            mode: "cors",
            cache: "no-cache",
            credentials: "same-origin",
            headers: {
              "Content-Type": "application/json",
            },
            redirect: "follow",
            referrerPolicy: "no-referrer",
            body: JSON.stringify(data),
          });
          return response.json();
        }

        async function handleSubmit(e) {
          console.log("submitting form");
          const title = document.getElementById("title").value;
          const author = document.getElementById("author").value;
          const body = document.getElementById("body").value;
          const response = await postData("/", { title, author, body });
          console.log(response);
          window.location.href = "/";
        }
        const submitButton = document.getElementById("submit");
        submitButton.addEventListener("click", handleSubmit);
      </script>
    </main>
  </body>
</html>

Edit Post Template

This is our fourth and last template needed to finish the blog.

Name this one edit-post.html. It's going to work exactly like the create post template, except it has two buttons. One to delete a post, and one to edit a post. It's also different because it is editing so it prefills all of the forms with values from the post being edited.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Edit {{.Post.Title}}</title>
    <style>
      html,
      body,
      main {
        height: 100%;
      }
      body {
        background-color: lightgray;
        font-family: arial;
        margin: 0.2rem;
      }
      main,
      header {
        display: flex;
        flex-direction: column;
        max-width: 600px;
        margin: auto;
      }
      h1 {
        font-size: 3rem;
      }
      h2 {
        font-size: 1.5rem;
      }
      p {
        font-size: 1rem;
      }
      ul {
        list-style: none;
        padding: 0;
      }
      a {
        font-weight: 600;
        color: #ff4f98;
        text-decoration: none;
      }
      a:hover {
        color: #ff529a;
        text-decoration: none;
      }
      input,
      textarea,
      button {
        margin: 0.5rem 0;
        border-radius: 4px;
        border: none;
        padding: 12px;
      }
      button {
        cursor: pointer;
      }
      textarea {
        flex: 1 1 0;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Edit {{.Post.Title}}</h1>
      <a href="/">Back</a>
      <button id="delete">Delete</button>
      <input name="title" id="title" value="{{.Post.Title}}" />
      <input name="author" id="author" value="{{.Post.Author}}" />
      <input
        name="postDate"
        id="postDate"
        value="{{.Post.DatePosted}}"
        disabled
      />
      <textarea name="body" id="body">{{.Post.Body}}</textarea>
      <button id="submit">Submit</button>
    </main>
    <script>
      async function postData(url = "", data = {}) {
        const response = await fetch(url, {
          method: "POST",
          mode: "cors",
          cache: "no-cache",
          credentials: "same-origin",
          headers: {
            "Content-Type": "application/json",
          },
          redirect: "follow",
          referrerPolicy: "no-referrer",
          body: JSON.stringify(data),
        });
        return response.json();
      }

      async function handleSubmit(e) {
        console.log("submitting form");
        const title = document.getElementById("title").value;
        const author = document.getElementById("author").value;
        const body = document.getElementById("body").value;
        const response = await postData("/{{.Post.Slug}}", {
          title,
          author,
          body,
        });
        console.log(response);
        window.location.href = "/{{.Post.Slug}}";
      }

      async function deleteData(url = "", data = {}) {
        const response = await fetch(url, {
          method: "DELETE",
          mode: "cors",
          cache: "no-cache",
          credentials: "same-origin",
          headers: {
            "Content-Type": "application/json",
          },
          redirect: "follow",
          referrerPolicy: "no-referrer",
        });
        return response.json();
      }

      async function handleDelete(e) {
        console.log("delete button clicked");
        const response = await deleteData("/{{.Post.Slug}}");
        console.log(response);
        window.location.href = "/";
      }

      const submitButton = document.getElementById("submit");
      submitButton.addEventListener("click", handleSubmit);

      const deleteButton = document.getElementById("delete");
      deleteButton.addEventListener("click", handleDelete);
    </script>
  </body>
</html>

main.go

Make the following changes to main.go to add handlers for the createPost, editPost, and deletePost functionality.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

	"github.com/boltdb/bolt"
	"github.com/gomarkdown/markdown"
	"github.com/gorilla/mux"
	"github.com/gosimple/slug"
	"github.com/microcosm-cc/bluemonday"
)

// SiteMetaData is general information about the Site
type SiteMetaData struct {
	Title       string
	Description string
}

// Post is a struct defining the data structure for a blog post.
type Post struct {
	Author     string    `json:"author,omitempty"`
	Body       string    `json:"body,omitempty"`
	DatePosted time.Time `json:"datePosted,omitempty"`
	Title      string    `json:"title,omitempty"`
	Slug       string    `json:"slug,omitempty"`
}

// PostMap is a map of posts with the slug as the key.
type PostMap map[string]Post

var siteMetaData = SiteMetaData{
	Title:       "Go Tiny Blog",
	Description: "A one file, simple to reason about blog designed to demonstrate the basic anatomy of a web app.",
}

// HomePageData is the data required to render the HTML template for the home page.
// It is made up of the site meta data, and a map of all of the posts.
type HomePageData struct {
	SiteMetaData SiteMetaData
	Posts        PostMap
}

// PostPageData is the data required to render the HTML template for the post page.
// It is made up of the site meta data, and a Post struct.
type PostPageData struct {
	SiteMetaData SiteMetaData
	Post         Post
	HTML         template.HTML
}

func main() {

	db, err := setupDB()
	defer db.Close()

	if err != nil {
		log.Println(err)
	}

	r := newRouter(db)
	// Create http server and run inside go routine for graceful shutdown.
	srv := &http.Server{
		Handler:      r,
		Addr:         "127.0.0.1:8000",
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	log.Println("Starting up..")

	// This code is all about gracefully shutting down the web server.
	// This allows the server to resolve any pending requests before shutting down.
	// This works by running the web server in a go routine.
	// The main function then continues and blocks waiting for a kill signal from the os.
	// It intercepts the kill signal, shuts down the server by calling the Shutdown method.
	// Then exits when that is done.
	go func() {
		if err := srv.ListenAndServe(); err != nil {
			log.Println(err)
		}
	}()

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	<-c
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
	defer cancel()

	srv.Shutdown(ctx)
	log.Println("Shutting down..")
	os.Exit(0)
}

// homeHandler returns the list of blog posts rendered in an HTML template.
func homeHandler(db *bolt.DB, t *template.Template) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		postData, err := listPosts(db)
		if err != nil {
			res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
			res.WriteHeader(http.StatusInternalServerError)
			res.Write([]byte("Could not list posts."))
			return
		}
		log.Println("Requested the home page.")
		res.Header().Set("Content-Type", "text/html; charset=UTF-8")
		res.WriteHeader(http.StatusOK)
		t.Execute(res, HomePageData{SiteMetaData: siteMetaData, Posts: postData})
	}

	return fn
}

// createPostPageHandler serves the UI for creating a post. It is a form that submits to the create post REST endpoint.
func createPostPageHandler(db *bolt.DB, t *template.Template) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		log.Println("Requested the create post page.")
		res.Header().Set("Content-Type", "text/html; charset=UTF-8")
		res.WriteHeader(http.StatusOK)
		t.Execute(res, HomePageData{SiteMetaData: siteMetaData})
	}

	return fn
}

// postHandler looks up a specific blog post and returns it as an HTML template.
func getPostHandler(db *bolt.DB, t *template.Template) http.HandlerFunc {

	fn := func(res http.ResponseWriter, r *http.Request) {
		// Get the URL param named slug from the response.
		slug := mux.Vars(r)["slug"]
		post, err := getPost(db, slug)
		if err != nil {
			res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
			res.WriteHeader(http.StatusNotFound)
			res.Write([]byte("404 Page Not Found"))
			return
		}
		log.Printf("Requested: %s by %s \n", post.Title, post.Author)
		unsafePostHTML := markdown.ToHTML([]byte(post.Body), nil, nil)
		postHTML := bluemonday.UGCPolicy().SanitizeBytes(unsafePostHTML)
		res.Header().Set("Content-Type", "text/html; charset=UTF-8")
		res.WriteHeader(http.StatusOK)
		t.Execute(res, PostPageData{SiteMetaData: siteMetaData, Post: *post, HTML: template.HTML(postHTML)})
	}
	return fn
}

// editPostPageHandler serves the UI for creating a post. It is a form that submits to the create post REST endpoint.
func editPostPageHandler(db *bolt.DB, t *template.Template) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		// Get the URL param named slug from the response.
		slug := mux.Vars(r)["slug"]
		post, err := getPost(db, slug)
		if err != nil {
			res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
			res.WriteHeader(http.StatusNotFound)
			res.Write([]byte("404 Page Not Found"))
			return
		}
		log.Printf("Requested edit page for: %s by %s \n", post.Title, post.Author)
		res.Header().Set("Content-Type", "text/html; charset=UTF-8")
		res.WriteHeader(http.StatusOK)
		t.Execute(res, PostPageData{SiteMetaData: siteMetaData, Post: *post})
	}

	return fn
}

// createPostHandler handles posted JSON data representing a new post, and stores it in the database.
// It creates a slug to use as a key using the title of the post.
// This implies in the current state of affairs that titles must be unique or the keys will overwrite each other.
func createPostHandler(db *bolt.DB) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		var post Post
		res.Header().Set("Content-Type", "text/plain; charset=UTF-8")
		// Reads in the body content from the post request safely limiting to max size.
		body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
		if err != nil {
			panic(err)
		}
		// Close the Reader.
		if err := r.Body.Close(); err != nil {
			panic(err)
		}

		// Convert the JSON to a Post struct and write it to the post variable created at the top
		// of the handler.
		if err := json.Unmarshal(body, &post); err != nil {
			res.Header().Set("Content-Type", "application/json; charset=UTF-8")
			res.WriteHeader(422) // unprocessable entity
			if err := json.NewEncoder(res).Encode(err); err != nil {
				panic(err)
			}
		}

		// Set the creation time stamp to the current server time.
		post.DatePosted = time.Now()

		// Create a URL safe slug from the timestamp and the title.
		autoSlug := fmt.Sprintf("%s-%s", slug.Make(post.DatePosted.Format(time.RFC3339)), slug.Make(post.Title))
		post.Slug = autoSlug

		if err = upsertPost(db, post, autoSlug); err != nil {
			res.WriteHeader(http.StatusInternalServerError)
			res.Write([]byte("Error writing to DB."))
			return
		}

		res.Header().Set("Content-Type", "application/json; charset=UTF-8")
		res.WriteHeader(http.StatusCreated)
		if err := json.NewEncoder(res).Encode(post); err != nil {
			panic(err)
		}
	}
	return fn
}

// modifyPostHandler is responsible for modifing the contents of a specific post.
// It accepts a new post object as JSON content in the request body.
// It writes the new post object to the URL slug value unlike the createPostHandler
// which generates a new slug using the post date and time. Notice this means you can not change the URI.
// This is left as homework for the reader.
func modifyPostHandler(db *bolt.DB) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		var post Post
		slug := mux.Vars(r)["slug"]
		res.Header().Set("Content-Type", "application/json; charset=UTF-8")
		body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
		if err != nil {
			panic(err)
		}
		if err := r.Body.Close(); err != nil {
			panic(err)
		}
		if err := json.Unmarshal(body, &post); err != nil {
			res.WriteHeader(422) // unprocessable entity
			if err := json.NewEncoder(res).Encode(err); err != nil {
				panic(err)
			}
		}
		post.Slug = slug
		post.DatePosted = time.Now()
		// Call the upsertPost function passing in the database, a post struct, and the slug.
		// If there is an error writing to the database write an error to the response and return.
		if err = upsertPost(db, post, slug); err != nil {
			res.WriteHeader(http.StatusInternalServerError)
			res.Write([]byte("Error writing to DB."))
			return
		}
		res.WriteHeader(http.StatusCreated)
		if err := json.NewEncoder(res).Encode(post); err != nil {
			panic(err)
		}
	}
	return fn
}

// DeletePostHandler deletes the post with the key matching the slug in the URL.
func deletePostHandler(db *bolt.DB) http.HandlerFunc {
	fn := func(res http.ResponseWriter, r *http.Request) {
		res.Header().Set("Content-Type", "application/json; charset=UTF-8")
		slug := mux.Vars(r)["slug"]
		if err := deletePost(db, slug); err != nil {
			panic(err)
		}
		res.WriteHeader(http.StatusOK)
		if err := json.NewEncoder(res).Encode(struct {
			Deleted bool
		}{
			true,
		}); err != nil {
			panic(err)
		}
	}
	return fn
}

// DATA STORE FUNCTIONS

// upsertPost writes a post to the boltDB KV store using the slug as a key, and a serialized post struct as the value.
// If the slug already exists the existing post will be overwritten.
func upsertPost(db *bolt.DB, post Post, slug string) error {

	// Marshal post struct into bytes which can be written to Bolt.
	buf, err := json.Marshal(post)
	if err != nil {
		return err
	}

	err = db.Update(func(tx *bolt.Tx) error {
		err := tx.Bucket([]byte("BLOG")).Bucket([]byte("POSTS")).Put([]byte(slug), []byte(buf))
		if err != nil {
			return fmt.Errorf("could not insert content: %v", err)
		}
		return nil
	})
	return err
}

// listPosts returns a map of posts indexed by the slug.
// TODO: We could we add pagination to this!
func listPosts(db *bolt.DB) (PostMap, error) {
	results := PostMap{}
	err := db.View(func(tx *bolt.Tx) error {
		// Assume bucket exists and has keys
		b := tx.Bucket([]byte("BLOG")).Bucket([]byte("POSTS"))

		c := b.Cursor()

		for k, v := c.First(); k != nil; k, v = c.Next() {
			post := Post{}
			if err := json.Unmarshal(v, &post); err != nil {
				panic(err)
			}
			results[string(k)] = post
		}

		return nil
	})
	if err != nil {
		return nil, err
	}
	return results, nil
}

// getPost gets a specific post from the database by the slug.
func getPost(db *bolt.DB, slug string) (*Post, error) {
	result := Post{}
	err := db.View(func(tx *bolt.Tx) error {
		// Assume bucket exists and has keys
		b := tx.Bucket([]byte("BLOG")).Bucket([]byte("POSTS"))
		v := b.Get([]byte(slug))
		if err := json.Unmarshal(v, &result); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &result, nil
}

// deletePost deletes a specific post by slug.
func deletePost(db *bolt.DB, slug string) error {
	err := db.Update(func(tx *bolt.Tx) error {
		err := tx.Bucket([]byte("BLOG")).Bucket([]byte("POSTS")).Delete([]byte(slug))
		if err != nil {
			return fmt.Errorf("could not delete content: %v", err)
		}
		return nil
	})
	return err
}

// INITIALIZATION FUNCTIONS
// setupDB sets up the database when the program start.
//  First it connects to the database, then it creates the buckets required to run the app if they do not exist.
func setupDB() (*bolt.DB, error) {
	db, err := bolt.Open("tinyblog.db", 0600, nil)
	if err != nil {
		return nil, fmt.Errorf("could not open db, %v", err)
	}
	err = db.Update(func(tx *bolt.Tx) error {
		root, err := tx.CreateBucketIfNotExists([]byte("BLOG"))
		if err != nil {
			return fmt.Errorf("could not create root bucket: %v", err)
		}
		_, err = root.CreateBucketIfNotExists([]byte("POSTS"))
		if err != nil {
			return fmt.Errorf("could not create post bucket: %v", err)
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("could not set up buckets, %v", err)
	}
	fmt.Println("DB Setup Done")
	return db, nil
}

// newRouter configures and sets up the gorilla mux router paths and connects the route to the handler function.
func newRouter(db *bolt.DB) *mux.Router {

	// Load and parse the html templates to be used.
	homeTemplate := template.Must(template.ParseFiles("templates/home.html"))
	postTemplate := template.Must(template.ParseFiles("templates/post.html"))
	editTemplate := template.Must(template.ParseFiles("templates/edit-post.html"))
	createTemplate := template.Must(template.ParseFiles("templates/create-post.html"))
	r := mux.NewRouter()
	r.StrictSlash(true)
	r.HandleFunc("/", homeHandler(db, homeTemplate)).Methods("GET")
	r.HandleFunc("/", createPostHandler(db)).Methods("POST")
	r.HandleFunc("/create", createPostPageHandler(db, createTemplate)).Methods("GET")
	r.HandleFunc("/{slug}", getPostHandler(db, postTemplate)).Methods("GET")
	r.HandleFunc("/{slug}", modifyPostHandler(db)).Methods("POST")
	r.HandleFunc("/{slug}", deletePostHandler(db)).Methods("DELETE")
	r.HandleFunc("/{slug}/edit", editPostPageHandler(db, editTemplate)).Methods("GET")
	return r
}

If you didn't already know Go this example might be a bit dense to get through. But what I hope you take away is how by combining 7 routes, and 7 handler functions and by wiring them up to the right URIs and with the right HTTP methods you can build a whole blog.

The other main take away should be that you don't need to be intimidated by web applications. They are literally just big routers for processing incoming request messages, and sending back outgoing response messages. Every single web framework you will ever run in to is at the end of the day just finding different ways to accomplish that same task.

In part 2 of this series on web applications we will learn about applying middleware, user authentication, static files, improving performance, and deploying a web app to the internet.