Listing data
OK, let’s move on and get our GET /v1/movies endpoint returning some real data.
For now, we’ll ignore any query string values provided by the client and return all movie records, sorted by movie ID. This will give us a solid base from which we can develop the more specialized functionality around filtering, sorting, and pagination.
Our aim in this chapter will be to get the endpoint to return a JSON response containing an array of all movies, similar to this:
{
"movies": [
{
"id": 1,
"title": "Moana",
"year": 2015,
"runtime": "107 mins",
"genres": [
"animation",
"adventure"
],
"version": 1
},
{
"id": 2,
"title": "Black Panther",
"year": 2018,
"runtime": "134 mins",
"genres": [
"sci-fi",
"action",
"adventure"
],
"version": 2
},
... etc.
]
}
Updating the application
To retrieve this data from our PostgreSQL database, let’s create a new GetAll() method on our database model which executes the following SQL query:
SELECT id, created_at, title, year, runtime, genres, version FROM movies ORDER BY id
Because we’re expecting this SQL query to return multiple records, we’ll need to run it using Go’s QueryContext() method. We already explained how this works in detail in Let’s Go, so let’s jump into the code:
package data ... // Create a new GetAll() method which returns a slice of movies. Although we're not // using them right now, we've set this up to accept the various filter parameters as // arguments. func (m MovieModel) GetAll(title string, genres []string, filters Filters) ([]*Movie, error) { // Construct the SQL query to retrieve all movie records. query := ` SELECT id, created_at, title, year, runtime, genres, version FROM movies ORDER BY id` // Create a context with a 3-second timeout. ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() // Use QueryContext() to execute the query. This returns a sql.Rows resultset // containing the result. rows, err := m.DB.QueryContext(ctx, query) if err != nil { return nil, err } // Importantly, defer a call to rows.Close() to ensure that the resultset is closed // before GetAll() returns. defer rows.Close() // Initialize an empty slice to hold the movie data. movies := []*Movie{} // Use rows.Next to iterate through the rows in the resultset. for rows.Next() { // Initialize an empty Movie struct to hold the data for an individual movie. var movie Movie // Scan the values from the row into the Movie struct. Again, note that we're // using the pq.Array() adapter on the genres field here. err := rows.Scan( &movie.ID, &movie.CreatedAt, &movie.Title, &movie.Year, &movie.Runtime, pq.Array(&movie.Genres), &movie.Version, ) if err != nil { return nil, err } // Add the Movie struct to the slice. movies = append(movies, &movie) } // After the rows.Next() loop has finished, call rows.Err() to retrieve any error // that was encountered during the iteration. if err = rows.Err(); err != nil { return nil, err } // If everything went OK, then return the slice of movies. return movies, nil }
Next up, we need to adapt the listMoviesHandler so that it calls the new GetAll() method to retrieve the movie data, and then writes this data as a JSON response.
If you’re following along, go ahead and update the handler like so:
package main ... func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request) { var input struct { Title string Genres []string data.Filters } v := validator.New() qs := r.URL.Query() input.Title = app.readString(qs, "title", "") input.Genres = app.readCSV(qs, "genres", []string{}) input.Filters.Page = app.readInt(qs, "page", 1, v) input.Filters.PageSize = app.readInt(qs, "page_size", 20, v) input.Filters.Sort = app.readString(qs, "sort", "id") input.Filters.SortSafelist = []string{"id", "title", "year", "runtime", "-id", "-title", "-year", "-runtime"} if data.ValidateFilters(v, input.Filters); !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return } // Call the GetAll() method to retrieve the movies, passing in the various filter // parameters. movies, err := app.models.Movies.GetAll(input.Title, input.Genres, input.Filters) if err != nil { app.serverErrorResponse(w, r, err) return } // Send a JSON response containing the movie data. err = app.writeJSON(w, http.StatusOK, envelope{"movies": movies}, nil) if err != nil { app.serverErrorResponse(w, r, err) } }
And now we should be ready to try this out.
Go ahead and restart the API, then when you make a GET /v1/movies request you should see the slice of movies returned by GetAll() rendered as a JSON array. Similar to this:
$ curl localhost:4000/v1/movies
{
"movies": [
{
"id": 1,
"title": "Moana",
"year": 2016,
"runtime": "107 mins",
"genres": [
"animation",
"adventure"
],
"version": 1
},
{
"id": 2,
"title": "Black Panther",
"year": 2018,
"runtime": "134 mins",
"genres": [
"sci-fi",
"action",
"adventure"
],
"version": 2
},
{
"id": 4,
"title": "The Breakfast Club",
"year": 1985,
"runtime": "97 mins",
"genres": [
"comedy"
],
"version": 5
}
]
}