Skip to content

Commit

Permalink
Merge 088c6c0 into 2b39596
Browse files Browse the repository at this point in the history
  • Loading branch information
mh-cbon committed Mar 28, 2019
2 parents 2b39596 + 088c6c0 commit b8f7179
Showing 1 changed file with 48 additions and 0 deletions.
48 changes: 48 additions & 0 deletions README.md
Expand Up @@ -53,13 +53,15 @@ As our application is simple, we will create only one table called users with th

Let’s use the following statement to create the database and the table.

```sql
CREATE DATABASE rest_api_example;
USE rest_api_example;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT NOT NULL
);
```

It is a very simple table but it’s ok for this example.

Expand All @@ -80,6 +82,7 @@ You can easily use go get to get it:

First of all, let’s create a file called app.go and add an App structure to hold our application. This structure provides references to the router and the database that we will use on our application. To make it testable let’s also create two methods to initialize and run the application:

```go
// app.go

package main
Expand All @@ -99,13 +102,15 @@ First of all, let’s create a file called app.go and add an App structure to ho
func (a *App) Initialize(user, password, dbname string) { }

func (a *App) Run(addr string) { }
```

The Initialize method is responsible for create a database connection and wire up the routes, and the Run method will simply start the application.

Note that we have to import both mux and mysql packages here.

Now, let’s create the main.go file which will contain the entry point for the application:

```go
// main.go

package main
Expand All @@ -117,11 +122,13 @@ Now, let’s create the main.go file which will contain the entry point for the

a.Run(":8080")
}
```

Note that on this step you need to set the username and password.

Now, let’s create a file called model.go which is used to define our user structure and provide some useful functions to deal with database operations.

```go
// model.go

package main
Expand Down Expand Up @@ -156,6 +163,7 @@ Now, let’s create a file called model.go which is used to define our user stru
func getUsers(db *sql.DB, start, count int) ([]user, error) {
return nil, errors.New("Not implemented")
}
```

At this point we should have a file structure like that:

Expand All @@ -171,6 +179,7 @@ As we are following the test-driven development (TDD) methodology, we need to wr

As we will run the tests using a database, we need to make sure the database is set up before running the tests and cleaned up after the tests. So let’s create the main_test.go file. In the main_test.go file let’s create the TestMain function which is executed before all tests and will do these stuff for us.

```go
// main_test.go

package main
Expand Down Expand Up @@ -214,6 +223,7 @@ As we will run the tests using a database, we need to make sure the database is
name VARCHAR(50) NOT NULL,
age INT NOT NULL
)`
```

Note that the global variable a represents the application that we want to test.

Expand All @@ -223,6 +233,7 @@ After run the tests we need to call the clearTable function to clean the databas

In order to run the tests we need to implement the Initialize function in the app.go file, to create a database connection and initialize the router. Now the Initialize function should look like this:

```go
// app.go

func (a *App) Initialize(user, password, dbname string) {
Expand All @@ -236,6 +247,7 @@ In order to run the tests we need to implement the Initialize function in the ap

a.Router = mux.NewRouter()
}
```

At this point even if we don’t have any tests we should be able to run go test without finding any runtime errors. Let’s try it out:

Expand All @@ -251,6 +263,7 @@ Executing this command should result something like this:

Let’s start testing the response of the /users endpoint with an empty table.

```go
// main_test.go

func TestEmptyTable(t *testing.T) {
Expand All @@ -265,13 +278,15 @@ Let’s start testing the response of the /users endpoint with an empty table.
t.Errorf("Expected an empty array. Got %s", body)
}
}
```

This test will delete all records in the users table and send a GET request to the /users endpoint.

We use the executeRequest function to execute the request, and checkResponseCode function to test that the HTTP response code is what we expect, and finally, we check the body of the response and check if it is what we expect.

So, let’s implement the executeRequest and checkResponseCode functions.

```go
// main_test.go

func executeRequest(req *http.Request) *httptest.ResponseRecorder {
Expand All @@ -286,6 +301,7 @@ So, let’s implement the executeRequest and checkResponseCode functions.
t.Errorf("Expected response code %d. Got %d\n", expected, actual)
}
}
```

Make sure you have imported the "net/http" and "net/http/httptest" packages and run the tests again. If everything goes well you should get something like this:

Expand All @@ -301,6 +317,7 @@ As expected, the tests will fail because we don’t have implemented anything ye

Let’s implement a test that tries to fetch a nonexistent user.

```go
// main_test.go

func TestGetNonExistentUser(t *testing.T) {
Expand All @@ -317,13 +334,15 @@ Let’s implement a test that tries to fetch a nonexistent user.
t.Errorf("Expected the 'error' key of the response to be set to 'User not found'. Got '%s'", m["error"])
}
}
```

This test basically tests two things: the status code which should be 404 and if the response contains the expected error message.

Note that in this step we need to import the "encoding/json" package to use the json.Unmarshal function.

Now, let’s implement a test to create a user.

```go
// main_test.go

func TestCreateUser(t *testing.T) {
Expand Down Expand Up @@ -353,13 +372,15 @@ Now, let’s implement a test to create a user.
t.Errorf("Expected user ID to be '1'. Got '%v'", m["id"])
}
}
```

In this test, we manually add a new user to the database and, by accessing the correspondent endpoint, we check if the status code is 201 (the resource was created) and if the JSON response contains the correct information that was added.

Note that in this step we need to import the "bytes" package to use the bytes.NewBuffer function.

Now, let’s implement a test to fetch an existing user.

```go
// main_test.go

func TestGetUser(t *testing.T) {
Expand All @@ -371,11 +392,13 @@ Now, let’s implement a test to fetch an existing user.

checkResponseCode(t, http.StatusOK, response.Code)
}
```

This test basically add a new user to the database and check if the correct endpoint results in an HTTP response with status code 200 (success).

In this test above we use the addUsers function which is used to add a new user to the database for the tests. So, let’s implement this function:

```go
// main_test.go

func addUsers(count int) {
Expand All @@ -388,11 +411,13 @@ In this test above we use the addUsers function which is used to add a new user
a.DB.Exec(statement)
}
}
```

Note that in this step we need to import the "strconv" package to use the strconv.Itoa function to convert an integer to a string.

Now, let’s test the update option:

```go
// main_test.go

func TestUpdateUser(t *testing.T) {
Expand Down Expand Up @@ -426,13 +451,15 @@ Now, let’s test the update option:
t.Errorf("Expected the age to change from '%v' to '%v'. Got '%v'", originalUser["age"], m["age"], m["age"])
}
}
```

In the above test, we basically add a new user to the database and then we use the correct endpoint to update it.

It tests if the status code is 200 indicating success and if the JSON response contains the updated details about the user.

And the last test, for now, will try to delete a user.

```go
// main_test.go

func TestDeleteUser(t *testing.T) {
Expand All @@ -452,6 +479,7 @@ And the last test, for now, will try to delete a user.
response = executeRequest(req)
checkResponseCode(t, http.StatusNotFound, response.Code)
}
```

In this test we basically create a new user and test if it exists in the database, then we user the correct endpoint to delete the user and checks if it was properly deleted.

Expand All @@ -463,6 +491,7 @@ All tests should fail but it’s ok because we did not implement the application

Let’s begin implementing the methods in the model.go file. These methods are responsible for executing the database statements and it can be implemented as follows:

```go
// model.go

func (u *user) getUser(db *sql.DB) error {
Expand Down Expand Up @@ -521,6 +550,7 @@ Let’s begin implementing the methods in the model.go file. These methods are r

return users, nil
}
```

The getUsers function fetches records from the users table and limits the number of records based on the count value passed by parameter. The start parameter determines how many records are skipped at the beginning.

Expand All @@ -530,6 +560,7 @@ The model is done, now we need to implement the App functions, including the **r

Let’s start creating the getUser function to fetch a single user.

```go
// app.go

func (a *App) getUser(w http.ResponseWriter, r *http.Request) {
Expand All @@ -553,11 +584,13 @@ Let’s start creating the getUser function to fetch a single user.

respondWithJSON(w, http.StatusOK, u)
}
```

This handler basically retrieves the id of the user from the requested URL and uses the getUser function, from the model, to fetch the user details.

If the user is not found it will respond with the status code 404. This function uses the respondWithError and respondWithJSON functions to process errors and normal responses. These functions are implemented as follows:

```go
// app.go

func respondWithError(w http.ResponseWriter, code int, message string) {
Expand All @@ -571,9 +604,11 @@ If the user is not found it will respond with the status code 404. This function
w.WriteHeader(code)
w.Write(response)
}
```

The rest of the handlers can be implemented in a similar manner:

```go
// app.go

func (a *App) getUsers(w http.ResponseWriter, r *http.Request) {
Expand All @@ -595,11 +630,13 @@ The rest of the handlers can be implemented in a similar manner:

respondWithJSON(w, http.StatusOK, users)
}
```

This handler uses the count and start parameters from the querystring to fetch count number of users, starting at position start in the database. By default, start is set to 0 and count is set to 10. If these parameters aren’t provided, this handler will respond with the first 10 users.

Let’s implement the handler to create a user.

```go
// app.go

func (a *App) createUser(w http.ResponseWriter, r *http.Request) {
Expand All @@ -618,11 +655,13 @@ Let’s implement the handler to create a user.

respondWithJSON(w, http.StatusCreated, u)
}
```

This handler assumes that the request body is a JSON object containing the details of the user to be created. It extracts that object into a user and then uses the createUser function.

The handler to update a user:

```go
// app.go

func (a *App) updateUser(w http.ResponseWriter, r *http.Request) {
Expand All @@ -649,11 +688,13 @@ The handler to update a user:

respondWithJSON(w, http.StatusOK, u)
}
```

This handler extracts the user details from the request body and the id from the URL, and uses the id and the body to update the user.

And the last handler that we will implement is used to delete a user.

```go
// app.go

func (a *App) deleteUser(w http.ResponseWriter, r *http.Request) {
Expand All @@ -672,11 +713,13 @@ And the last handler that we will implement is used to delete a user.

respondWithJSON(w, http.StatusOK, map[string]string{"result": "success"})
}
```

This handler extracts the id from the URL and uses it to delete the corresponding user.

Now that we have all handlers implemented we must define the routes which will use them.

```go
// app.go

func (a *App) initializeRoutes() {
Expand All @@ -686,11 +729,13 @@ Now that we have all handlers implemented we must define the routes which will u
a.Router.HandleFunc("/user/{id:[0-9]+}", a.updateUser).Methods("PUT")
a.Router.HandleFunc("/user/{id:[0-9]+}", a.deleteUser).Methods("DELETE")
}
```

The routes are defined based on the API specification defined earlier. The {id:[0-9]+} part of the path indicates that Gorilla Mux should treat process a URL only if the id is a number. For all matching requests, Gorilla Mux then stores the the actual numeric value in the id variable.

Now we just need to implement the Run function and call initializeRoutes from the Initialize method.

```go
// app.go

func (a *App) Initialize(user, password, dbname string) {
Expand All @@ -709,9 +754,11 @@ Now we just need to implement the Run function and call initializeRoutes from th
func (a *App) Run(addr string) {
log.Fatal(http.ListenAndServe(addr, a.Router))
}
```

Remember to import all packages needed.

```go
// app.go

import (
Expand All @@ -725,6 +772,7 @@ Remember to import all packages needed.
_ "github.com/go-sql-driver/mysql"
"github.com/gorilla/mux"
)
```

The final version of the app.go file should look like this: [https://github.com/kelvins/GoApiTutorial/blob/master/app.go](https://github.com/kelvins/GoApiTutorial/blob/master/app.go)

Expand Down

0 comments on commit b8f7179

Please sign in to comment.