10 min read

RESTful Routing Revealed

In this chapter, we explore efficient routing with Gorilla Mux, emphasizing clear and concise route names.
Two roads diverged in a yellow wood

REST, or Representational State Transfer, might sound like a techy mouthful. At its core is an architectural style that defines a set of constraints to be used when creating web services. It's the guiding principle behind a lot of the web services we use today, enabling different systems to communicate with each other using standard conventions.

It is based on an architectural style for designing networked applications. It utilizes a stateless, client-server communication model, where each request from a client to a server must contain all the information needed to understand and process the request.

REST uses standard HTTP methods, such as GET, POST, PUT, and DELETE, and operates over resources identified by URLs. These resources can be any object, data, or service that can be named and addressed. When a RESTful API is called, the server transfers a representation of the state of the resource to the requester, usually in the form of JSON or XML but HTML can also be used. In fact, REST is format-agnostic, meaning it does not dictate a specific data format for responses.

Key principles of REST include statelessness (each request contains all the information and there's no session state stored on the server), client-server architecture (separation of concerns between client and server), cacheability (responses can be cached to improve performance), uniform interface (consistent and limited set of well-defined methods to operate on resources), layered system (components do not need to know beyond their layer), and code-on-demand (optional ability for servers to extend client functionality by transferring executable code). This approach simplifies scaling and allows for independent evolution of the client and server.

HTML-based Workflows vs. JSON API Endpoints

Web development can be approached in various ways, but for the sake of our series, we're zeroing in on two primary methods: HTML-based workflows and traditional JSON API endpoints.

HTML-based workflows primarily focus on serving pages directly from the server. Think of it as a direct dialogue where the server responds with a full-fledged HTML page every time a user makes a request. This approach provides a seamless experience even when JavaScript is disabled, maintaining website accessibility and functionality.

On the other hand, JSON API endpoints are the backbone of many modern web apps. These endpoints return data in a standardized format (JSON) that frontend applications then consume and render. It's like a server giving raw ingredients (data) to a chef (frontend) who then cooks up a dish (visual webpage).

Gorilla Library

Enter Gorilla, our chosen toolkit for this implementation. The Gorilla library stands out for its power and simplicity in handling routes and requests in Go. As we go deeper into our series, you'll see just how pivotal Gorilla becomes, laying the foundation for our RESTful routing and ensuring smooth communication between web components.

Laying the Groundwork

Standard API URLs and Their Responses

Before diving into the practical stuff, it's essential to familiarize ourselves with standard API URLs. Typically, web services adhere to a specific pattern:

- **GET** `/resource`: Retrieve a list of resources.
- **GET** `/resource/:id`: Obtain details of a specific resource.
- **POST** `/resource`: Create a new resource.
- **PUT/PATCH** `/resource/:id`: Update an existing resource.
- **DELETE** `/resource/:id`: Remove a resource.

These routes are the heart of our application, letting clients interact with our server, request data, and make changes as necessary.

For each action, there are standard HTTP response codes to indicate the success or failure of the operation:

  • 200 OK: The request was successful.
  • 201 Created: A new resource was successfully created.
  • 204 No Content: The request was successful, but there's nothing to send back.
  • 400 Bad Request: The request was invalid or cannot be served.
  • 404 Not Found: The requested resource couldn't be found.
  • 500 Internal Server Error: A generic error message when an unexpected condition is encountered.

A Closer Look at "Resource

In our outlined API URLs, the term resource might initially seem abstract, but it embodies the pivotal entities within our application. Within Plykan, our app's realm, the mention of a resource alludes to core segments like boards, columns, stickers, and so forth. Even a user would squarely fall under the umbrella of a resource. It's best to conceptualize a resource as any central entity or component that our app orchestrates, and that users might wish to engage with through our API.

On Singular vs. Plural Resource Naming

The naming convention for resources in URLs typically leans towards the plural form. It serves to represent an entire collection of those entities, think of boards or users. Singular forms, on the other hand, are commonly reserved for scenarios in specific programming contexts, but when it comes to URL paths in our setup, we'll predominantly stick to the plural. For instance, /boards would indicate an endpoint addressing all boards, while /boards/:id would pertain to a specific one.Such conventions not only provide clarity but also simplify our API's navigation, enhancing both comprehension and consistency.

Handling JavaScript-Disabled Clients

In the modern web, JavaScript enhances the user experience, but not all users have it enabled. Our application adapts to this by recognizing when JavaScript is disabled, even though it's uncommon in standard user environments. For instance, in cases where resource-constrained hardware is utilized, or specific needs require such configuration, our application remains adaptable and functional.

When JavaScript is disabled, the server will provide a dedicated confirmation page for tasks requiring confirmation, such as deletions. This page will include a 'confirm' button to ensure a consistent experience in these situations.

Deletion: Soft vs. Hard

A critical aspect of our app's architecture is how deletions are handled. While the specifics will be covered later, it's worth noting that our application will have the ability to configure two types of deletions:

  • Soft Deletion: The data isn't truly removed from the database. Instead, it's marked as 'deleted,' making it invisible to standard operations but recoverable if needed.
  • Hard Deletion: Data is permanently removed from the database and cannot be recovered.

This flexibility ensures that we can adjust the application's behavior based on specific use cases or requirements.

Additionally, we will introduce a purge feature that offers the ability to trigger permanent deletion of soft-deleted records. This feature can be activated manually or scheduled through a cron process. Purge essentially ensures the irreversible removal of data that has been marked as deleted in our database, providing a comprehensive solution for data management in line with specific use cases or requirements.

HTML-focused CRUD Operations

Over the years, the rise of SPAs (Single Page Applications) built using powerful frameworks like React, Angular, or Vue became the mainstay of web development. These frameworks, although robust, introduced complexities, especially for solo developers and solopreneurs desiring to prototype and get an MVP running quickly.

Fast forward to 2023,, a library like htmx is emerging as a game-changer, although it's worth noting that this approach is not entirely new. Developed by the creator of intercooler.js, htmx is gaining popularity due to its ability to offer SPA-like dynamism without the associated complexities. Similar libraries such as Turbolinks have also embraced these principles. With htmx, developers can create interactive interfaces seamlessly within a familiar HTML-centric environment. The clear advantages include:

Unified Codebase: Avoid constant toggling between front-end and back-end codebases.

Simplicity and Speed: Streamline your development process, relying more on the browser's inherent capabilities and less on intricate client-side logic.

Reduced Learning Overhead: Instead of learning an entirely new framework on top of JavaScript, htmx merges effortlessly with HTML.

While the vast majority of modern web development may have heavily invested in SPAs, our approach with Plykan will be slightly unconventional. We are focusing more on serving HTML pages directly, aiming for a fluid user experience without becoming overly reliant on JavaScript. This ensures our application remains accessible even when JavaScript is disabled on the client side. The Gorilla library, in particular, will play a pivotal role in managing our routes and CRUD operations efficiently.

Now, with htmx's rise, we're also introduced to the concept of HATEOAS - Hypermedia as the engine of application state. This principle means that our application's server will provide all the necessary information for the client to interact with it, using hypermedia as the medium. A client adhering to HATEOAS doesn't need prior knowledge of the API's structure, making the API exploration more intuitive.

In Plykan's development, the combination of Gorilla and htmx is set to seamlessly enhance our HTML-based workflows, making our application more dynamic. As we continue, you'll see how these tools empower our development process, fostering both intuition and efficiency.

4. Initial Code Structure

Starting with a structured approach can be beneficial, even if the initial steps seem elementary. In the early phase of Plykan, our code will primarily reside within a single file: main.go. This simplifies our starting point, making it easier to understand and modify. But as our application grows, we'll certainly refactor and distribute our code logically across multiple files and folders.

Coding the Routes

Before diving into the new code, let's set up our environment for this fresh start:

Create a new Git branch

It's a good practice to keep features or changesets contained within their own branches. This allows for easier code reviews, testing, and possible rollbacks if something goes awry.

$ git checkout -b feature/routing

Clean up our project

Remember the "Hello, World!" code we started with in the previous chapter? Let's replace that with our new routing logic.

Open main.go and clear its content and later add the following code.

package main

import (
	"fmt"
	"net/http"
	"github.com/gorilla/mux"
)

func main() {
	r := mux.NewRouter()

	r.HandleFunc("/boards", boardsIndexHandler).Methods("GET")
	r.HandleFunc("/boards/new", newBoardHandler).Methods("GET")
	r.HandleFunc("/boards", createBoardHandler).Methods("POST")
	r.HandleFunc("/boards/{id}", showBoardHandler).Methods("GET")
	r.HandleFunc("/boards/{id}/edit", editBoardHandler).Methods("GET")
	r.HandleFunc("/boards/{id}", updateBoardHandler).Methods("PUT")
	r.HandleFunc("/boards/{id}", deleteBoardHandler).Methods("DELETE")
	r.HandleFunc("/boards/{id}/confirm-delete", deleteConfirmHandler).Methods("GET")

	http.ListenAndServe(":8080", r)
}

func boardsIndexHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Listing all boards.")
}

func newBoardHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Displaying form for new board.")
}

func createBoardHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Creating a new board.")
}

func showBoardHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]
	fmt.Fprintf(w, "Showing board with ID: %s.", id)
}

func editBoardHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]
	fmt.Fprintf(w, "Editing board with ID: %s.", id)
}

func updateBoardHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]
	fmt.Fprintf(w, "Updating board with ID: %s.", id)
}

func deleteBoardHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]
	fmt.Fprintf(w, "Deleting board with ID: %s.", id)
}

func deleteConfirmHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]
	fmt.Fprintf(w, "Confirm delete for board with ID: %s.", id)
}

https://github.com/solutioncrafting/plykan/blob/feature/routing/main.go

You can view the full code with detailed comments in the repository at https://github.com/solutioncrafting/plykan/tree/feature/routing For clarity and format preservation, lengthy comments have been omitted here.

📝 A Friendly Reminder: While it might be tempting to quickly copy and paste the code snippets provided, I strongly encourage you to type them out yourself. By actively engaging with the material in this way, you're not only solidifying your understanding but also building muscle memory. This hands-on approach will help you become more fluent and efficient in your coding over time. Remember, the journey to mastery is paved with practice.

Running the project

$ go run main.go

Then, if we browse to http:/localhost:8080/boards we should be able to see the response from boardsIndexHandler

Listing all boards browser view

While if instead we go to http:/localhost:8080/boards/1 the browser should show something like this:

Showing board with ID 1 browser view

You can try other routes but keep in mind that if the handlers are associated with POST, PUT or DELETE methods the procedure is not as direct. We'll see later, when we replace the basic implementation of the handlers with the one that corresponds to each one, how we can send them requests of those other types.

Now let's see what we've done...

Clean up our project

1. Importing Necessary Packages

import (
	"fmt"
	"net/http"
	
	"github.com/gorilla/mux"
)

Same as in the previous Hello World example.

This block brings in the packages we need:

  • fmt for formatting our output.
  • net/http for handling HTTP operations.
  • github.com/gorilla/mux is the Gorilla Mux router which provides us with the tools to define request routes and handle URL parameters.

2. Defining Routes with Gorilla Mux

// Board routes  
r.HandleFunc("/boards", boardsIndexHandler).Methods("GET")
(...)
r.HandleFunc("/boards", createBoardHandler).Methods("POST")
(...)
r.HandleFunc("/boards/{id}", updateBoardHandler).Methods("PUT")
r.HandleFunc("/boards/{id}", deleteBoardHandler).Methods("DELETE")
(...)

In this segment, we start by creating a new router instance using Gorilla Mux. After this, routes are defined using the HandleFunc method. The first argument is the URL pattern, and the second is the handler function which should execute when the pattern is matched.

The Methods("XXX") part is a chaining method that specifies which HTTP method (e.g., GET, POST, PUT) the route should respond to. If a user tries to access the endpoint using a method not specified, the request will not be routed to the handler.

3. Parameterized Routes

r.HandleFunc("/boards/{id}", showBoardHandler).Methods("GET")
(...)

Some routes, like the one above, have {id} in their URL pattern. This is a placeholder indicating that the route expects a variable value at that position in the URL. When a request matches this pattern, the actual value in place {id} can be accessed and used in the handler.

4. Handlers and Their Basic Functionality

Each handler is a function that takes in a ResponseWriter and a Request object. For now, our handlers are primarily using the fmt.Fprint or fmt.Fprintf function to send a text response back to the client's browser.

func boardsIndexHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Listing all boards.")
}
(...)

In cases where we're dealing with a parameterized route as we saw before, we use Gorilla Mux's Vars function to extract the value(s) from the URL:

vars := mux.Vars(r)
id := vars["id"]

Here, mux.Vars(r) retrieves a map of the URL parameters and their values. We then access the value of the {id} parameter by indexing into the map with the key "id".

5. Starting the Server

http.ListenAndServe(":8080", r)

Finally, we call http.ListenAndServe to start our server on port 8080 and tell it to use our Gorilla Mux router, r, for routing incoming requests.

Up Next: Structuring and Streamlining

Next, we're diving into organizing our Go project better. We'll explore the roles of the internal and pkg directories. Then, we'll get our hands on Go's text/html template system for rendering dynamic content to the browser. To round things off, we'll introduce a Makefile to make our development tasks a breeze.

Stay tuned as we level up our project.

References

Previous article: Go for Gold: A Classic Warm-Up Exercise