README ¶
Storyboard
Introduction
Use this sample in Go trainings to introduce attendees to basic concepts of web API development in Go.
Getting started
Goal: Create a basic web server returning JSON response.
- Create empty directory basicwebapi
touch main.go
go mod init github.com/rstropek/golang-samples/basicwebapi
- Add starter code to main.go
package main
import (
"log"
"net/http"
)
// Define a home handler function which writes a byte slice containing
// hard-coded JSON as the response body.
func home(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("{ \"foo\": \"bar\" }"))
}
func main() {
// Initialize a new servemux, then register the home function as
// the handler for the "/" URL pattern.
mux := http.NewServeMux()
mux.HandleFunc("/", home)
// Use the http.ListenAndServe() function to start a new web server.
port := ":4000"
log.Printf("Starting server on %s", port)
err := http.ListenAndServe(port, mux)
log.Fatal(err)
}
- Run app
go run .
- Test it (Tip: Use REST Client for that)
GET http://localhost:4000/
- Discussions:
- What is a multiplexer in general and what is
ServeMux
- See also simple web server sample
- How to make the webserver gracefully shut down? See advanced web server sample
Add Customer Struct
Goal: Add a struct and serialize it to JSON for getting a HTTP response body.
- Add package for handling GUIDs and decimal values
go get github.com/google/uuid
go get github.com/shopspring/decimal
- Add
customer
struct
// ...
import (
"encoding/json"
"log"
"net/http"
"github.com/google/uuid"
"github.com/shopspring/decimal"
)
// ...
// Setup structure for storing customer data
type customer struct {
CustomerID uuid.UUID `json:"customerID,omitempty"`
CompanyName string `json:"customerName"`
ContactName string `json:"contactName"`
Country string `json:"country"`
HourlyRate decimal.Decimal `json:"hourlyRate"`
}
- Change home function to return object encoded in JSON
// Return encoded demo customer in JSON
func home(w http.ResponseWriter, r *http.Request) {
cid, _ := uuid.NewUUID()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&customer {
CustomerID: cid,
CompanyName: "Acme Corp",
ContactName: "Foo Bar",
Country: "DEU",
HourlyRate: decimal.NewFromInt(42),
})
}
- Test it
GET http://localhost:4000/
- Discussions:
- What are well-known struct tags
json.NewEncoder
takes a writer, i.e. we can usehttp.ResponseWriter
- See a functionally richer, simple API in simple web API sample
- Package web API in Docker image
Add More Powerful Router
- Add Gorilla MUX package
go get github.com/gorilla/mux
- Change mux to Gorilla
// ...
import (
"encoding/json"
"log"
"net/http"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/shopspring/decimal"
)
// ...
func main() {
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/", home)
// ...
}
- Test it
GET http://localhost:4000/
- Discussions:
- There are so many routers for Go, we use gorilla/mux
Store Customers in In-Memory Map
- Remove
home
method
// ...
import (
"encoding/json"
"log"
"net/http"
"sync"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/shopspring/decimal"
)
// ...
// Store map of customers in memory
var customers = make(map[uuid.UUID]customer, 0)
// Mutex serializing access to customers. We need this mutex because
// go serves all incoming HTTP requests in their own goroutine. Therefore,
// it is possible if not likely that handlers will run concurrently.
// As concurrent reading without writing is allowed, we could optimize
// our code using `RWMutex` (https://golang.org/pkg/sync/#RWMutex).
// However, this is out of scope for this sample. We will use RWMutex
// in the next sample (Go-Kit).
var customersMutex = &sync.Mutex{}
// ...
// getCustomersArray returns all stored customers as an array
func getCustomersArray() []customer {
// Lock customers while accessing it
customersMutex.Lock()
defer customersMutex.Unlock()
// Convert map of customers into array
values := make([]customer, len(customers))
i := 0
for _, v := range customers {
values[i] = v
i++
}
return values
}
func getCustomers(w http.ResponseWriter, r *http.Request) {
// Return all customers
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(getCustomersArray())
}
// ...
func main() {
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/customers", getCustomers).Methods("GET")
// Use the http.ListenAndServe() function to start a new web server.
port := ":4000"
log.Printf("Starting server on %s", port)
err := http.ListenAndServe(port, mux)
log.Fatal(err)
}
- Discussions:
- Maps on Go by example
- Mutexes on Go by example
- Sample for implementing a singleton repository with channels see advanced web API sample
- When to use mutexes, when channels?
Command-Line Arguments
// ...
import (
"encoding/json"
"flag"
"log"
"net/http"
"sync"
"fmt"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/shopspring/decimal"
)
// ...
func main() {
// Parse command-line arguments
var portFlag = flag.Uint("p", 4000, "Port number for starting server")
flag.Parse()
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/customers", getCustomers).Methods("GET")
// Use the http.ListenAndServe() function to start a new web server.
log.Printf("Starting server on %d", *portFlag)
err := http.ListenAndServe(fmt.Sprintf(":%d", *portFlag), mux)
log.Fatal(err)
}
-
Test it:
go run . -p 8081
-
Discussions:
- Command-line arguments on Go by Example
- Command-line flags on Go by Example
- There are so many packages for building CLIs
Get Single Customer
// ...
func getCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
// Note http.Error shortcut. Use it to send a non-200 status code and
// plain-text response body.
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Lock customers while accessing it
customersMutex.Lock()
defer customersMutex.Unlock()
// Check if customer with given ID exists
if c, ok := customers[cid]; ok {
// Return customer
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(c)
return
}
// Customer hasn't been found
http.NotFound(w, r)
}
// newUUID returns a new UUID and ignores potential errors
func newUUID() uuid.UUID {
r, _ := uuid.NewUUID()
return r
}
func main() {
// ...
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/customers", getCustomers).Methods("GET")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", getCustomer).Methods("GET")
// ...
}
- Test it
# @name customers
GET http://localhost:4000/customers
###
@customerID = {{customers.response.body.$[0].customerID}}
GET http://localhost:4000/customers/{{customerID}}
###
GET http://localhost:4000/customers/00000000-0000-0000-0000-000000000000
Add Customer
// ...
func addCustomer(w http.ResponseWriter, r *http.Request) {
// Decode customer data from request body
var c = customer{}
if json.NewDecoder(r.Body).Decode(&c) != nil {
http.Error(w, "Could not deserialize customer from HTTP body", http.StatusBadRequest)
return
}
// Make sure that incoming custer data is sane
if c.CustomerID != uuid.Nil {
http.Error(w, "CustomerID must be empty", http.StatusBadRequest)
return
}
if len(c.CompanyName) == 0 {
http.Error(w, "Company name must not be empty", http.StatusBadRequest)
return
}
if len(c.ContactName) == 0 {
http.Error(w, "Contact name must not be empty", http.StatusBadRequest)
return
}
if len(c.Country) != 3 {
http.Error(w, "Country name must be three characters long (use ISO 3166-1 Alpha-3 code)", http.StatusBadRequest)
return
}
if decimal.NewFromInt(0).GreaterThan(c.HourlyRate) {
http.Error(w, "Hourly rate must be >= 0", http.StatusBadRequest)
return
}
// Assign new customer ID
c.CustomerID = newUUID()
// Lock customers while accessing it
customersMutex.Lock()
defer customersMutex.Unlock()
// Add customer to our list
customers[c.CustomerID] = c
// Return customer
w.Header().Set("Location", fmt.Sprintf("/customers/%s", c.CustomerID))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(c)
}
func main() {
// ...
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/customers", getCustomers).Methods("GET")
mux.HandleFunc("/customers", addCustomer).Methods("POST")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", getCustomer).Methods("GET")
// ...
}
- Test it
# ...
###
POST http://localhost:4000/customers
{
"customerName": "Acme Corp",
"contactName": "Foo Bar",
"country": "DEU",
"hourlyRate": "42"
}
Delete Customer
// ...
func deleteCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Lock customers while accessing it
customersMutex.Lock()
defer customersMutex.Unlock()
// Check if customer with given ID exists
if _, ok := customers[cid]; ok {
delete(customers, cid)
w.WriteHeader(http.StatusNoContent)
return
}
// Customer hasn't been found
http.NotFound(w, r)
}
func main() {
// ...
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/customers", getCustomers).Methods("GET")
mux.HandleFunc("/customers", addCustomer).Methods("POST")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", getCustomer).Methods("GET")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", deleteCustomer).Methods("DELETE")
// ...
}
# ...
###
DELETE http://localhost:4000/customers/{{customerID}}
Update Customer
// ...
func patchCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Decode customer data from request body
var c = customer{}
if json.NewDecoder(r.Body).Decode(&c) != nil {
http.Error(w, "Could not deserialize customer from HTTP body", http.StatusBadRequest)
return
}
// If customer ID was specified, it must match the customer ID from path
if c.CustomerID != uuid.Nil && cid != c.CustomerID {
http.Error(w, "Cannot update customer ID", http.StatusBadRequest)
return
}
// Lock customers while accessing it
customersMutex.Lock()
defer customersMutex.Unlock()
// Check if customer with given ID exists
if cOld, ok := customers[cid]; ok {
// Update specified fields
if len(c.CompanyName) > 0 {
cOld.CompanyName = c.CompanyName
}
if len(c.ContactName) > 0 {
cOld.ContactName = c.ContactName
}
if len(c.Country) > 0 {
if len(c.Country) != 3 {
http.Error(w, "Country name must be three characters long (use ISO 3166-1 Alpha-3 code)", http.StatusBadRequest)
return
}
cOld.Country = c.Country
}
if c.HourlyRate != decimal.NewFromInt(0) {
if decimal.NewFromInt(0).GreaterThan(c.HourlyRate) {
http.Error(w, "Hourly rate must be >= 0", http.StatusBadRequest)
return
}
cOld.HourlyRate = c.HourlyRate
}
// Update customer in in-memory store
customers[cid] = cOld
// Return updated customer data
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cOld)
}
}
func main() {
// ...
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/customers", getCustomers).Methods("GET")
mux.HandleFunc("/customers", addCustomer).Methods("POST")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", getCustomer).Methods("GET")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", deleteCustomer).Methods("DELETE")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", patchCustomer).Methods("PATCH")
// ...
}
Add Query Parameter
// ...
import (
// ...
"sort"
// ...
)
// ...
type byCompanyName []customer
func (c byCompanyName) Len() int { return len(c) }
func (c byCompanyName) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c byCompanyName) Less(i, j int) bool { return c[i].CompanyName < c[j].CompanyName }
func getCustomers(w http.ResponseWriter, r *http.Request) {
custArray := getCustomersArray()
orderBy := r.FormValue("orderBy")
if len(orderBy) > 0 {
if orderBy != "companyName" {
http.Error(w, "Currently, we can only order by companyName", http.StatusBadRequest)
return
}
sort.Sort(byCompanyName(custArray))
}
// Return all customers
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(custArray)
}
// ...
func main() {
// ...
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) { panic("Something really bad happened...") }).Methods("GET")
mux.HandleFunc("/customers", getCustomers).Methods("GET")
mux.HandleFunc("/customers", getCustomers).Queries("orderBy", "{orderBy}").Methods("GET")
// ...
}
- Try it
# ...
###
GET http://localhost:4000/customers?orderBy=companyName
Add Middleware
- Add negroni
go get github.com/urfave/negroni
go get github.com/rs/cors
- Add classic middleware and CORS
// ...
import (
// ...
"github.com/urfave/negroni"
"github.com/rs/cors"
)
// ...
func main() {
// ...
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) { panic("Something really bad happened...") }).Methods("GET")
// ...
n := negroni.Classic()
n.UseHandler(mux)
n.Use(cors.AllowAll())
// Use the http.ListenAndServe() function to start a new web server.
log.Printf("Starting server on %d", *portFlag)
err := http.ListenAndServe(fmt.Sprintf(":%d", *portFlag), n)
log.Fatal(err)
}
- Try it
# ...
###
GET http://localhost:4000/panic
-
Create public subdirectory
-
Add demo client
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo Client</title>
</head>
<body>
<ul id="customers" />
<script>
(async () => {
const cElem = document.getElementById("customers");
let html = "";
const result = await fetch("/customers");
const custs = await result.json();
for (const c of custs) {
html += `<li>${c.customerName}</li>`;
}
cElem.innerHTML = html;
})();
</script>
</body>
</html>
-
Try it by opening
http://localhost:4000/index.html
in your browser -
Discussions:
Split Into Multiple Files
- Create customerrepository.go
package main
import (
"sync"
"github.com/google/uuid"
"github.com/shopspring/decimal"
)
// Setup structure for storing customer data
type customer struct {
CustomerID uuid.UUID `json:"customerID,omitempty"`
CompanyName string `json:"customerName"`
ContactName string `json:"contactName"`
Country string `json:"country"`
HourlyRate decimal.Decimal `json:"hourlyRate"`
}
// Store map of customers in memory
var customers = make(map[uuid.UUID]customer, 0)
// Mutex serializing access to customers
var customersMutex = &sync.Mutex{}
// getCustomersArray returns all stored customers as an array
func getCustomersArray() []customer {
// Lock customers while accessing it
customersMutex.Lock()
defer customersMutex.Unlock()
// Convert map of customers into array
values := make([]customer, len(customers))
i := 0
for _, v := range customers {
values[i] = v
i++
}
return values
}
type byCompanyName []customer
func (c byCompanyName) Len() int { return len(c) }
func (c byCompanyName) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c byCompanyName) Less(i, j int) bool { return c[i].CompanyName < c[j].CompanyName }
- Create handlers.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/shopspring/decimal"
)
func getCustomers(w http.ResponseWriter, r *http.Request) {
custArray := getCustomersArray()
orderBy := r.FormValue("orderBy")
if len(orderBy) > 0 {
if orderBy != "companyName" {
http.Error(w, "Currently, we can only order by companyName", http.StatusBadRequest)
return
}
sort.Sort(byCompanyName(custArray))
}
// Return all customers
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(custArray)
}
func getCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Lock customers while accessing it
customersMutex.Lock()
defer customersMutex.Unlock()
// Check if customer with given ID exists
if c, ok := customers[cid]; ok {
// Return customer
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(c)
return
}
// Customer hasn't been found
http.NotFound(w, r)
}
// newUUID returns a new UUID and ignores potential errors
func newUUID() uuid.UUID {
r, _ := uuid.NewUUID()
return r
}
func addCustomer(w http.ResponseWriter, r *http.Request) {
// Decode customer data from request body
var c = customer{}
if json.NewDecoder(r.Body).Decode(&c) != nil {
http.Error(w, "Could not deserialize customer from HTTP body", http.StatusBadRequest)
return
}
// Make sure that incoming custer data is sane
if c.CustomerID != uuid.Nil {
http.Error(w, "CustomerID must be empty", http.StatusBadRequest)
return
}
if len(c.CompanyName) == 0 {
http.Error(w, "Company name must not be empty", http.StatusBadRequest)
return
}
if len(c.ContactName) == 0 {
http.Error(w, "Contact name must not be empty", http.StatusBadRequest)
return
}
if len(c.Country) != 3 {
http.Error(w, "Country name must be three characters long (use ISO 3166-1 Alpha-3 code)", http.StatusBadRequest)
return
}
if decimal.NewFromInt(0).GreaterThan(c.HourlyRate) {
http.Error(w, "Hourly rate must be >= 0", http.StatusBadRequest)
return
}
// Assign new customer ID
c.CustomerID = newUUID()
// Lock customers while accessing it
customersMutex.Lock()
defer customersMutex.Unlock()
// Add customer to our list
customers[c.CustomerID] = c
// Return customer
w.Header().Set("Location", fmt.Sprintf("/customers/%s", c.CustomerID))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(c)
}
func deleteCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Lock customers while accessing it
customersMutex.Lock()
defer customersMutex.Unlock()
// Check if customer with given ID exists
if _, ok := customers[cid]; ok {
delete(customers, cid)
w.WriteHeader(http.StatusNoContent)
return
}
// Customer hasn't been found
http.NotFound(w, r)
}
func patchCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Decode customer data from request body
var c = customer{}
if json.NewDecoder(r.Body).Decode(&c) != nil {
http.Error(w, "Could not deserialize customer from HTTP body", http.StatusBadRequest)
return
}
// If customer ID was specified, it must match the customer ID from path
if c.CustomerID != uuid.Nil && cid != c.CustomerID {
http.Error(w, "Cannot update customer ID", http.StatusBadRequest)
return
}
// Lock customers while accessing it
customersMutex.Lock()
defer customersMutex.Unlock()
// Check if customer with given ID exists
if cOld, ok := customers[cid]; ok {
// Update specified fields
if len(c.CompanyName) > 0 {
cOld.CompanyName = c.CompanyName
}
if len(c.ContactName) > 0 {
cOld.ContactName = c.ContactName
}
if len(c.Country) > 0 {
if len(c.Country) != 3 {
http.Error(w, "Country name must be three characters long (use ISO 3166-1 Alpha-3 code)", http.StatusBadRequest)
return
}
cOld.Country = c.Country
}
if c.HourlyRate != decimal.NewFromInt(0) {
if decimal.NewFromInt(0).GreaterThan(c.HourlyRate) {
http.Error(w, "Hourly rate must be >= 0", http.StatusBadRequest)
return
}
cOld.HourlyRate = c.HourlyRate
}
// Update customer in in-memory store
customers[cid] = cOld
// Return updated customer data
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cOld)
}
}
- Shorten main.go
package main
import (
"flag"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/rs/cors"
"github.com/shopspring/decimal"
"github.com/urfave/negroni"
)
func main() {
// Parse command-line arguments
var portFlag = flag.Uint("p", 4000, "Port number for starting server")
flag.Parse()
// Add one demo record
cid := newUUID()
customers[cid] = customer{
CustomerID: cid,
CompanyName: "Acme Corp",
ContactName: "Foo Bar",
Country: "DEU",
HourlyRate: decimal.NewFromInt(42),
}
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) { panic("Something really bad happened...") }).Methods("GET")
mux.HandleFunc("/customers", getCustomers).Methods("GET")
mux.HandleFunc("/customers", getCustomers).Queries("orderBy", "{orderBy}").Methods("GET")
mux.HandleFunc("/customers", addCustomer).Methods("POST")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", getCustomer).Methods("GET")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", deleteCustomer).Methods("DELETE")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", patchCustomer).Methods("PATCH")
n := negroni.Classic()
n.UseHandler(mux)
n.Use(cors.AllowAll())
// Use the http.ListenAndServe() function to start a new web server.
log.Printf("Starting server on %d", *portFlag)
err := http.ListenAndServe(fmt.Sprintf(":%d", *portFlag), n)
log.Fatal(err)
}
- Discussions:
- Where to put business logic? Is e.g. validation logic of incoming requests at the right spot in the code above?
Convert Customer Repository in Package
-
Create subfolder customerrepository
-
Move customerrepository.go into new folder
-
Change code of customerrepository.go
package customerrepository
import (
"sync"
"github.com/google/uuid"
"github.com/shopspring/decimal"
)
// Customer holds data of a customer record
type Customer struct {
CustomerID uuid.UUID `json:"customerID,omitempty"`
CompanyName string `json:"customerName"`
ContactName string `json:"contactName"`
Country string `json:"country"`
HourlyRate decimal.Decimal `json:"hourlyRate"`
}
// CustomerRepository is an in-memory repository of customers
type CustomerRepository struct {
// Store map of customers in memory
customers map[uuid.UUID]Customer
// Mutex serializing access to customers. We need this mutex because
// go serves all incoming HTTP requests in their own goroutine. Therefore,
// it is possible if not likely that handlers will run concurrently.
// As concurrent reading without writing is allowed, we could optimize
// our code using `RWMutex` (https://golang.org/pkg/sync/#RWMutex).
// However, this is out of scope for this sample.
customersMutex *sync.Mutex
}
// NewCustomerRepository creates a customer repository
func NewCustomerRepository() CustomerRepository {
return CustomerRepository{
customers: make(map[uuid.UUID]Customer, 0),
customersMutex: &sync.Mutex{},
}
}
// GetCustomerByID looks for a customer with a given ID
func (cr CustomerRepository) GetCustomerByID(cid uuid.UUID) (*Customer, bool) {
// Lock customers while accessing it
cr.customersMutex.Lock()
defer cr.customersMutex.Unlock()
// Check if customer with given ID exists
if c, ok := cr.customers[cid]; ok {
return &c, true
}
return nil, false
}
// GetCustomersArray returns all stored customers as an array
func (cr CustomerRepository) GetCustomersArray() []Customer {
// Lock customers while accessing it
cr.customersMutex.Lock()
defer cr.customersMutex.Unlock()
// Convert map of customers into array
values := make([]Customer, len(cr.customers))
i := 0
for _, v := range cr.customers {
values[i] = v
i++
}
return values
}
// AddCustomer adds a customer to the repository
func (cr CustomerRepository) AddCustomer(c Customer) {
// Lock customers while accessing it
cr.customersMutex.Lock()
defer cr.customersMutex.Unlock()
// Add customer to our list
cr.customers[c.CustomerID] = c
}
// DeleteCustomerByID removes a customer with a given ID
func (cr CustomerRepository) DeleteCustomerByID(cid uuid.UUID) bool {
// Lock customers while accessing it
cr.customersMutex.Lock()
defer cr.customersMutex.Unlock()
// Check if customer with given ID exists
if _, ok := cr.customers[cid]; ok {
delete(cr.customers, cid)
return true
}
return false
}
// PatchCustomer patches a customer with the given values
func (cr CustomerRepository) PatchCustomer(cid uuid.UUID, c Customer) (*Customer, bool) {
// Lock customers while accessing it
cr.customersMutex.Lock()
defer cr.customersMutex.Unlock()
// Check if customer with given ID exists
if cOld, ok := cr.customers[cid]; ok {
// Update specified fields
if len(c.CompanyName) > 0 {
cOld.CompanyName = c.CompanyName
}
if len(c.ContactName) > 0 {
cOld.ContactName = c.ContactName
}
if len(c.Country) > 0 {
cOld.Country = c.Country
}
if c.HourlyRate != decimal.NewFromInt(0) {
cOld.HourlyRate = c.HourlyRate
}
// Update customer in in-memory store
cr.customers[cid] = cOld
return &cOld, true
}
return nil, false
}
// ByCompanyName is used for sorting customers by company name
type ByCompanyName []Customer
func (c ByCompanyName) Len() int { return len(c) }
func (c ByCompanyName) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ByCompanyName) Less(i, j int) bool { return c[i].CompanyName < c[j].CompanyName }
- Adjust code in handlers.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/rstropek/golang-samples/web-api/customerrepository"
"github.com/shopspring/decimal"
)
func getCustomers(w http.ResponseWriter, r *http.Request) {
custArray := repo.GetCustomersArray()
orderBy := r.FormValue("orderBy")
if len(orderBy) > 0 {
if orderBy != "companyName" {
http.Error(w, "Currently, we can only order by companyName", http.StatusBadRequest)
return
}
sort.Sort(customerrepository.ByCompanyName(custArray))
}
// Return all customers
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(custArray)
}
func getCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Check if customer with given ID exists
if c, ok := repo.GetCustomerByID(cid); ok {
// Return customer
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(c)
return
}
// Customer hasn't been found
http.NotFound(w, r)
}
// newUUID returns a new UUID and ignores potential errors
func newUUID() uuid.UUID {
r, _ := uuid.NewUUID()
return r
}
func addCustomer(w http.ResponseWriter, r *http.Request) {
// Decode customer data from request body
var c = customerrepository.Customer{}
if json.NewDecoder(r.Body).Decode(&c) != nil {
http.Error(w, "Could not deserialize customer from HTTP body", http.StatusBadRequest)
return
}
// Make sure that incoming custer data is sane
if c.CustomerID != uuid.Nil {
http.Error(w, "CustomerID must be empty", http.StatusBadRequest)
return
}
if len(c.CompanyName) == 0 {
http.Error(w, "Company name must not be empty", http.StatusBadRequest)
return
}
if len(c.ContactName) == 0 {
http.Error(w, "Contact name must not be empty", http.StatusBadRequest)
return
}
if len(c.Country) != 3 {
http.Error(w, "Country name must be three characters long (use ISO 3166-1 Alpha-3 code)", http.StatusBadRequest)
return
}
if decimal.NewFromInt(0).GreaterThan(c.HourlyRate) {
http.Error(w, "Hourly rate must be >= 0", http.StatusBadRequest)
return
}
// Assign new customer ID
c.CustomerID = newUUID()
// Add customer to our list
repo.AddCustomer(c)
// Return customer
w.Header().Set("Location", fmt.Sprintf("/customers/%s", c.CustomerID))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(c)
}
func deleteCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Delete customer
if repo.DeleteCustomerByID(cid) {
w.WriteHeader(http.StatusNoContent)
return
}
// Customer hasn't been found
http.NotFound(w, r)
}
func patchCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Decode customer data from request body
var c = customerrepository.Customer{}
if json.NewDecoder(r.Body).Decode(&c) != nil {
http.Error(w, "Could not deserialize customer from HTTP body", http.StatusBadRequest)
return
}
// If customer ID was specified, it must match the customer ID from path
if c.CustomerID != uuid.Nil && cid != c.CustomerID {
http.Error(w, "Cannot update customer ID", http.StatusBadRequest)
return
}
if len(c.Country) > 0 && len(c.Country) != 3 {
http.Error(w, "Country name must be three characters long (use ISO 3166-1 Alpha-3 code)", http.StatusBadRequest)
return
}
if c.HourlyRate != decimal.NewFromInt(0) && decimal.NewFromInt(0).GreaterThan(c.HourlyRate) {
http.Error(w, "Hourly rate must be >= 0", http.StatusBadRequest)
return
}
if cNew, ok := repo.PatchCustomer(cid, c); ok {
// Return updated customer data
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cNew)
return
}
// Customer hasn't been found
http.NotFound(w, r)
}
- Adjust code in main.go
package main
import (
"flag"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/rs/cors"
"github.com/rstropek/golang-samples/web-api/customerrepository"
"github.com/shopspring/decimal"
"github.com/urfave/negroni"
)
var repo = customerrepository.NewCustomerRepository()
func main() {
// Parse command-line arguments
var portFlag = flag.Uint("p", 4000, "Port number for starting server")
flag.Parse()
// Add one demo record
cid := newUUID()
repo.AddCustomer(customerrepository.Customer{
CustomerID: cid,
CompanyName: "Acme Corp",
ContactName: "Foo Bar",
Country: "DEU",
HourlyRate: decimal.NewFromInt(42),
})
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) { panic("Something really bad happened...") }).Methods("GET")
mux.HandleFunc("/customers", getCustomers).Methods("GET")
mux.HandleFunc("/customers", getCustomers).Queries("orderBy", "{orderBy}").Methods("GET")
mux.HandleFunc("/customers", addCustomer).Methods("POST")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", getCustomer).Methods("GET")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", deleteCustomer).Methods("DELETE")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", patchCustomer).Methods("PATCH")
n := negroni.Classic()
n.UseHandler(mux)
n.Use(cors.AllowAll())
// Use the http.ListenAndServe() function to start a new web server.
log.Printf("Starting server on %d", *portFlag)
err := http.ListenAndServe(fmt.Sprintf(":%d", *portFlag), n)
log.Fatal(err)
}
-
Try it
-
Discussions:
- Recap basics about nested packages based on simple modules sample
Add Unit Tests For Customer Repository
- Add customerrepository_test.go
package customerrepository
import (
"sort"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestAddCustomer(t *testing.T) {
cr := NewCustomerRepository()
cr.AddCustomer(Customer{})
assert.Equal(t, 1, len(cr.customers))
}
func TestGetCustomersArray(t *testing.T) {
cr := NewCustomerRepository()
cr.AddCustomer(Customer{})
assert.Equal(t, 1, len(cr.GetCustomersArray()))
}
func TestGetCustomerByID(t *testing.T) {
cr := NewCustomerRepository()
cr.AddCustomer(Customer{CustomerID: uuid.Nil})
_, ok := cr.GetCustomerByID(uuid.Nil)
assert.True(t, ok)
}
func TestDeleteCustomerByID(t *testing.T) {
cr := NewCustomerRepository()
cr.AddCustomer(Customer{CustomerID: uuid.Nil})
cr.DeleteCustomerByID(uuid.Nil)
assert.Equal(t, 0, len(cr.customers))
}
func TestPatchCustomer(t *testing.T) {
cr := NewCustomerRepository()
cr.AddCustomer(Customer{
CustomerID: uuid.Nil,
CompanyName: "Acme Corp",
})
cr.PatchCustomer(uuid.Nil, Customer{CompanyName: "Foo Bar"})
assert.Equal(t, "Foo Bar", cr.customers[uuid.Nil].CompanyName)
}
func TestOrderByCompanyName(t *testing.T) {
cr := NewCustomerRepository()
cr.AddCustomer(Customer{CompanyName: "B"})
cr.AddCustomer(Customer{CompanyName: "A"})
c := cr.GetCustomersArray()
sort.Sort(ByCompanyName(c))
assert.Equal(t, "A", c[0].CompanyName)
}
-
Try it:
go test .
-
Discussions:
- Recap basics about unit testing based on simple modules sample
Convert Customer API Handlers in Package
-
Create subfolder customerhandlers
-
Move customerhandlers.go into new folder
-
Change code of customerhandlers.go
package customerhandlers
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/rstropek/golang-samples/web-api/customerrepository"
"github.com/shopspring/decimal"
)
// CustomerHandlers represents functions handling HTTP requests for customers management web api
type CustomerHandlers struct {
repo customerrepository.CustomerRepository
}
// NewCustomerHandlers creates a customer handler object
func NewCustomerHandlers(repo customerrepository.CustomerRepository) CustomerHandlers {
return CustomerHandlers{
repo: repo,
}
}
// GetCustomers returns all customers
func (ch CustomerHandlers) GetCustomers(w http.ResponseWriter, r *http.Request) {
custArray := ch.repo.GetCustomersArray()
orderBy := r.FormValue("orderBy")
if len(orderBy) > 0 {
if orderBy != "companyName" {
http.Error(w, "Currently, we can only order by companyName", http.StatusBadRequest)
return
}
sort.Sort(customerrepository.ByCompanyName(custArray))
}
// Return all customers
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(custArray)
}
// GetCustomer returns a single customer based on a given customer ID
func (ch CustomerHandlers) GetCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Check if customer with given ID exists
if c, ok := ch.repo.GetCustomerByID(cid); ok {
// Return customer
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(c)
return
}
// Customer hasn't been found
http.NotFound(w, r)
}
// newUUID returns a new UUID and ignores potential errors
func newUUID() uuid.UUID {
r, _ := uuid.NewUUID()
return r
}
// AddCustomer adds a customer
func (ch CustomerHandlers) AddCustomer(w http.ResponseWriter, r *http.Request) {
// Decode customer data from request body
var c = customerrepository.Customer{}
if json.NewDecoder(r.Body).Decode(&c) != nil {
http.Error(w, "Could not deserialize customer from HTTP body", http.StatusBadRequest)
return
}
// Make sure that incoming custer data is sane
if c.CustomerID != uuid.Nil {
http.Error(w, "CustomerID must be empty", http.StatusBadRequest)
return
}
if len(c.CompanyName) == 0 {
http.Error(w, "Company name must not be empty", http.StatusBadRequest)
return
}
if len(c.ContactName) == 0 {
http.Error(w, "Contact name must not be empty", http.StatusBadRequest)
return
}
if len(c.Country) != 3 {
http.Error(w, "Country name must be three characters long (use ISO 3166-1 Alpha-3 code)", http.StatusBadRequest)
return
}
if decimal.NewFromInt(0).GreaterThan(c.HourlyRate) {
http.Error(w, "Hourly rate must be >= 0", http.StatusBadRequest)
return
}
// Assign new customer ID
c.CustomerID = newUUID()
// Add customer to our list
ch.repo.AddCustomer(c)
// Return customer
w.Header().Set("Location", fmt.Sprintf("/customers/%s", c.CustomerID))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(c)
}
// DeleteCustomer deletes a customer based on a given ID
func (ch CustomerHandlers) DeleteCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Delete customer
if ch.repo.DeleteCustomerByID(cid) {
w.WriteHeader(http.StatusNoContent)
return
}
// Customer hasn't been found
http.NotFound(w, r)
}
// PatchCustomer patches a customer based on a given ID and new field values
func (ch CustomerHandlers) PatchCustomer(w http.ResponseWriter, r *http.Request) {
// Get customer ID from path
cid, err := uuid.Parse(mux.Vars(r)["id"])
if err != nil {
http.Error(w, "Invalid customer ID format", http.StatusBadRequest)
return
}
// Decode customer data from request body
var c = customerrepository.Customer{}
if json.NewDecoder(r.Body).Decode(&c) != nil {
http.Error(w, "Could not deserialize customer from HTTP body", http.StatusBadRequest)
return
}
// If customer ID was specified, it must match the customer ID from path
if c.CustomerID != uuid.Nil && cid != c.CustomerID {
http.Error(w, "Cannot update customer ID", http.StatusBadRequest)
return
}
if len(c.Country) > 0 && len(c.Country) != 3 {
http.Error(w, "Country name must be three characters long (use ISO 3166-1 Alpha-3 code)", http.StatusBadRequest)
return
}
if c.HourlyRate != decimal.NewFromInt(0) && decimal.NewFromInt(0).GreaterThan(c.HourlyRate) {
http.Error(w, "Hourly rate must be >= 0", http.StatusBadRequest)
return
}
if cNew, ok := ch.repo.PatchCustomer(cid, c); ok {
// Return updated customer data
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cNew)
return
}
// Customer hasn't been found
http.NotFound(w, r)
}
- Adjust code in main.go
package main
import (
"github.com/google/uuid"
"github.com/rstropek/golang-samples/web-api/customerhandlers"
"flag"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/rs/cors"
"github.com/rstropek/golang-samples/web-api/customerrepository"
"github.com/shopspring/decimal"
"github.com/urfave/negroni"
)
func main() {
// Parse command-line arguments
var portFlag = flag.Uint("p", 4000, "Port number for starting server")
flag.Parse()
// Create customer repository
repo := customerrepository.NewCustomerRepository()
// Add one demo record
cid, _ := uuid.NewUUID()
repo.AddCustomer(customerrepository.Customer{
CustomerID: cid,
CompanyName: "Acme Corp",
ContactName: "Foo Bar",
Country: "DEU",
HourlyRate: decimal.NewFromInt(42),
})
// Create handlers
ch := customerhandlers.NewCustomerHandlers(repo)
// Initialize a new Gorilla mux, then register the home function as
// the handler for the "/" URL pattern.
mux := mux.NewRouter()
mux.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) { panic("Something really bad happened...") }).Methods("GET")
mux.HandleFunc("/customers", ch.GetCustomers).Methods("GET")
mux.HandleFunc("/customers", ch.GetCustomers).Queries("orderBy", "{orderBy}").Methods("GET")
mux.HandleFunc("/customers", ch.AddCustomer).Methods("POST")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", ch.GetCustomer).Methods("GET")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", ch.DeleteCustomer).Methods("DELETE")
mux.HandleFunc("/customers/{id:[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}}", ch.PatchCustomer).Methods("PATCH")
n := negroni.Classic()
n.UseHandler(mux)
n.Use(cors.AllowAll())
// Use the http.ListenAndServe() function to start a new web server.
log.Printf("Starting server on %d", *portFlag)
err := http.ListenAndServe(fmt.Sprintf(":%d", *portFlag), n)
log.Fatal(err)
}
Add Sample Unit Test for Customer API Handlers
- Add customerhandlers_test.go
package customerhandlers
import (
"encoding/json"
"github.com/stretchr/testify/assert"
"github.com/rstropek/golang-samples/web-api/customerrepository"
"net/http"
"net/http/httptest"
"testing"
)
func TestGetCustomers(t *testing.T) {
// Here we use the existing customer repository. In practice, you would probably
// use a mocking framework like https://github.com/stretchr/testify. However, proper
// mocking for unit tests is out of scope here.
repo := customerrepository.NewCustomerRepository()
repo.AddCustomer(customerrepository.Customer{CompanyName: "Foo Bar"})
ch := NewCustomerHandlers(repo)
// Create a request to pass to our handler
req, _ := http.NewRequest("GET", "/", nil)
// Create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response
rr := httptest.NewRecorder()
handler := http.HandlerFunc(ch.GetCustomers)
// Our handlers satisfy http.Handler, so we can call their ServeHTTP method
// directly and pass in our Request and ResponseRecorder
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
assert.Equal(t, http.StatusOK, rr.Code)
// Check content type
assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
// Check the JSON result
result := make([]customerrepository.Customer, 0)
json.NewDecoder(rr.Body).Decode(&result)
assert.Equal(t, 1, len(result))
assert.Equal(t, "Foo Bar", result[0].CompanyName)
}
-
Try it:
go test .
-
Discussions:
- There are so many packages for testing codebases
Centralize HTTP Response Building
- Add interface for building JSON object results in customerhandlers.go
// ...
// ObjectResultWriter writes a given object to the HTTP response
type ObjectResultWriter interface {
WriteObjectResult(w http.ResponseWriter, object interface{})
}
// CustomerHandlers represents functions handling HTTP requests for customers management web api
type CustomerHandlers struct {
repo customerrepository.CustomerRepository
orw ObjectResultWriter
}
// NewCustomerHandlers creates a customer handler object
func NewCustomerHandlers(repo customerrepository.CustomerRepository, orw ObjectResultWriter) CustomerHandlers {
return CustomerHandlers{
repo: repo,
orw: orw,
}
}
// ...
// GetCustomers returns all customers
func (ch CustomerHandlers) GetCustomers(w http.ResponseWriter, r *http.Request) {
// ...
// Return all customers
ch.orw.WriteObjectResult(w, custArray)
}
// ...
// GetCustomer returns a single customer based on a given customer ID
func (ch CustomerHandlers) GetCustomer(w http.ResponseWriter, r *http.Request) {
// ...
// Check if customer with given ID exists
if c, ok := ch.repo.GetCustomerByID(cid); ok {
// Return customer
ch.orw.WriteObjectResult(w, c)
return
}
// ...
}
// ...
// AddCustomer adds a customer
func (ch CustomerHandlers) AddCustomer(w http.ResponseWriter, r *http.Request) {
// ...
ch.orw.WriteObjectResult(w, c)
}
// ...
// PatchCustomer patches a customer based on a given ID and new field values
func (ch CustomerHandlers) PatchCustomer(w http.ResponseWriter, r *http.Request) {
// ...
if cNew, ok := ch.repo.PatchCustomer(cid, c); ok {
// Return updated customer data
ch.orw.WriteObjectResult(w, cNew)
return
}
// ...
}
- Adjust main.go
// ...
type responseWriter struct {}
func (r responseWriter) WriteObjectResult(w http.ResponseWriter, object interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(object)
}
func main() {
// ...
// Create handlers
ch := customerhandlers.NewCustomerHandlers(repo, responseWriter{})
// ...
}
Documentation ¶
There is no documentation for this package.
Click to show internal directories.
Click to hide internal directories.