RESTful API Documentation with Go and chi docgen Package
An important chore that gets neglected by developers is writing documentation for their RESTful APIs. Often, this task ends up being assigned lower priority than other tasks, such as building a new feature or modifying an existing feature. Although it delivers no immediate tangible value to end users like features, documentation produces intangible value for developers and their companies. By having detailed information about a RESTful API's endpoints, developers can quickly know how to obtain authorization for protected endpoints, access and interact with certain resources, format the data that's required in a request's body, etc. Ultimately, the value in documentation comes from increased developer productivity and saved development time. The more developers the documentation serves, the more value the documentation produces. Fortunately, you don't have to spend any time or effort to manually build and maintain documentation from scratch. There are open source tools like Swagger that automate the process of designing and generating RESTful API documentation for developers. These tools: Best of all, most of these tools only need, as input, the source code of the RESTful API or a JSON representation of the RESTful API that's compliant with the OpenAPI Specification . Or, these tools can get fed other representations of the RESTful API that are based on alternative API specifications like RAML ( R ESTful A PI M odeling L anguage). After generating the documentation, all that's left is to host and share the documentation. If you built your RESTful API with the Go chi library, then you can automatically generate routing documentation with one of its optional subpackages: docgen . Below, I'm going to show you how to leverage docgen to: To get started, clone the following repository to your local machine: This repository contains source code for a simple RESTful API built with Go and chi . The router uses a middleware stack that consists of the following middlewares: Additionally, render.SetContentType(render.ContentTypeJSON) forces the Content-Type of responses to be application/json . Most of these middlewares are ported from Goji , a minimalistic web framework for Go, and they are built with native Go packages. The router defines several endpoints for a resource that represents posts: To start up the RESTful API, first install the project's dependencies. Then, compile and execute the code. docgen lets you print a list of available routes to STDOUT at runtime, like so: This gives an overview of the resources that can be accessed from this RESTful API. To log routing documentation to STDOUT, first add the github.com/go-chi/docgen dependency to the list of imported packages and dependencies in main.go , like so: Note #1 : If you encounter the error message cannot use r (variable of type *"github.com/go-chi/chi".Mux) as type "github.com/go-chi/chi/v5".Routes in argument to docgen.JSONRoutesDoc: , then you should change the imported dependency from github.com/go-chi/chi to "github.com/go-chi/chi/v5 . Note #2 : If you encounter the error message http: panic serving 127.0.0.1:50114: runtime error: slice bounds out of range [-1:] , then you should change the imported dependency from github.com/go-chi/chi/middleware to github.com/go-chi/chi/v5/middleware . Then, inside of the main() function, call the method docgen.PrintRoutes() after mounting the sub-router defined on the postsResource struct to the main router, like so: Stop the RESTful API service and re-run the following commands to install the newly added dependency ( github.com/go-chi/docgen ) and restart the RESTful API service: Now, the RESTful API's routes get logged to STDOUT. Since documentation should be shared with other developers, let's output the routing information to a pre-formatted Markdown document. Using the docgen.MarkdownRoutesDoc() method, docgen crawls the router to build a route tree. For each route and subroute, docgen collects: Then, using this tree, docgen structures and outputs the Markdown as a string. docgen will link each middleware and handler function to its implementation in the source code (the file and line where the implementation can be located). The docgen.MarkdownRoutesDoc() method accepts two arguments: Note : Any fields omitted from the struct are zero-valued. Unlike logging the route documentation to STDOUT, the Markdown should not be generated every time the RESTful API service starts up, especially if you want to write the stringified Markdown to a separate file. Instead, let's generate the Markdown only when the user passes a specific flag to the go run command. First, let's add the flag and errors packages to the list of imported packages and dependencies in main.go , like so: Just before the main() function, declare a string flag docs with a default value of an empty string (second argument) and a short description (third argument). Note : This flag will support three values: markdown , json and raml . -docs=markdown will output the routing information to a pre-formatted Markdown document, while -docs=json and -docs=raml will output JSON and RAML representations of the RESTful API respectively. We will implement the json and raml flag values in the next section of this tutorial. At the start of the main() function body, call the flag.Parse() method to parse the command-line flags into any flags defined before the main() function. At the end of the main() function body, after calling the docgen.PrintRoutes() method, check if the docs flag has been set to markdown . If so, then call the docgen.MarkdownRoutesDoc() method to generate the Markdown and write it to a routes.md file, like so: Note #1 : f, err := os.Create("routes.md") does not follow the same pattern as the other conditional statements (of having a declaration precede conditionals). Otherwise, f.Close() would cause the error undefined: f since f would only be scoped to its corresponding conditional statement's branches. For the statement to follow the same pattern as the other conditional statements, you would need to declare the variables f and err outside and replace the short declaration with a standard assignment. Note #2 : Even if the routes.md file does not exist, os.Remove will return an error remove routes.md: no such file or directory . Therefore, to prevent this error from causing the program to exit, check that the error is not an os.ErrNotExist error via the errors.Is() method. Now, add a new rule to the Makefile that compiles and executes the code with the -docs=markdown flag. ( Makefile ) Also, add the following code to the top of the Makefile so that docgen can detect the GOPATH environment variable when the go run command is executed via a make command. ( Makefile ) Note : Without this, you will encounter the error message ERROR: docgen: unable to determine your $GOPATH . When you run the make command with the target gen_docs_md , Go generates the routing documentation in Markdown and outputs it to a routes.md file: Below is what the Markdown file should look like. The bulleted middleware and handler functions link directly to their implementation in the source code (file and line). ( routes.md ) Having JSON and RAML representations of a RESTful API gives other software and tools the ability to understand the RESTful API's endpoints, middleware stack, etc. These representations can be consumed to create beautiful, custom UIs for displaying the documentation, build testing and scaffolding tools, etc. The APIs provided by docgen for generating JSON and RAML representations of the RESTful API are quite different. Therefore, let's start with the much simpler task of generating the JSON representation of the RESTful API. In the main() function, add another conditional branch ( else if statement) to the conditional statement *docs == "markdown" that checks if the docs flag is set to json . If so, then call the docgen.JSONRoutesDoc() method to generate the JSON representation and write it to a routes.json file, like so: Now, add a new rule to the Makefile that compiles and executes the code with the -docs=json flag. ( Makefile ) When you run the make command with the target gen_docs_json , Go generates the JSON representation and outputs it to a routes.json file: Below is what the JSON file should look like. The JSON data organizes the routes in a hierarchical order and contains information like the different HTTP methods allowed for a given route, the handler function and middleware stack that's called for a given endpoint, the location of the handler function in the source code, any comments written above the handler function's definition, etc. ( routes.json ) Note : Although the JSON representation of the RESTful API that's generated by docgen does not fully adhere to the OpenAPI specification , you can still apply a few transformations to make it compliant and work with tools like Swagger UI. For the RAML representation, we need to add the strings package and two more dependencies to the list of imported packages and dependencies in main.go : Then, in the main() function, add another conditional branch ( else if statement) to the conditional statement *docs == "markdown" that checks if the docs flag is set to raml . If so, then perform the following steps to generate the RAML representation of the RESTful API: Note : The fields in the raml.RAML struct come directly from the RAML specification, and they describe some of the basic aspects of the RESTful API like its title and version. This information ends up in the root section of the outputted RAML document. If you are not familiar with RAML, then please visit the RAML documentation . Now, add a new rule to the Makefile that compiles and executes the code with the -docs=raml flag. ( Makefile ) Stop the RESTful API service and install the newly added dependencies. When you run the make command with the target gen_docs_raml , Go generates the RAML representation and outputs it to a routes.raml file: Below is what the RAML file should look like. Notice how docgen organizes the routes in a similar hierarchical order like what's seen in the JSON representation of the RESTful API, but in a YAML-based format. To turn this RAML file into an interactive, documentation UI that can be shared with others, let's use the raml2html HTML documentation generator. You can install raml2html globally via npm and run its CLI tool to generate the documentation UI from RAML. However, if you do not want to install raml2html globally on your local machine, then you can also run a Dockerfile that containerizes raml2html . Run the following docker command to run the raml2html CLI tool in a new container, giving it the routes.raml file as an input and having it output a routes.raml.html file. Note : If you encounter the error message docker: Error response from daemon: Mounts denied: The path <full_project_directory_path> is not shared from the host and is not known to Docker. You can configure shared paths from Docker -> Preferences... -> Resources -> File Sharing. , then you should add the full path of the project directory (whatever gets printed by the pwd ) to the list of directories that can be bind mounted into Docker containers. After that's completed, open a new terminal session and re-run the docker command. Once the routes.raml.html file is generated (located in the root of the project directory), open this file in a browser to see the documentation UI. If you find yourself stuck at any point while working through this tutorial, then feel free to visit the main branch of this GitHub repository here for the code. Explore and try out other automated documentation solutions. If you want to learn more advanced back-end web development techniques with Go, then check out the Reliable Webservers with Go course by Nat Welch, a site reliability engineer at Time by Ping (and formerly a site reliability engineer at Google), and Steve McCarthy, a senior software engineer at Etsy.