Let's Go Further CRUD operations › Updating a movie
Previous · Contents · Next
Chapter 7.4.

Updating a movie

In this chapter, we’ll continue building up our application and add a brand-new endpoint that allows clients to update the data for a specific movie.

Method URL Pattern Handler Action
GET /v1/healthcheck healthcheckHandler Show application information
POST /v1/movies createMovieHandler Create a new movie
GET /v1/movies/:id showMovieHandler Show the details of a specific movie
PUT /v1/movies/:id updateMovieHandler Update the details of a specific movie

More precisely, we’ll set up the endpoint so that a client can edit the title, year, runtime and genres values for a movie. In our project the id and created_at values should never change once they’ve been created, and the version value isn’t something that the client should control, so we won’t allow those fields to be edited.

For now, we’ll configure this endpoint so that it performs a full replacement of the values for a movie. This means that the client will need to provide values for all editable fields in their JSON request body… even if they only want to change one of them.

For example, if a client wants to add the genre sci-fi to the movie Black Panther in our database, they would need to send a JSON request body which looks like this:

{
    "title": "Black Panther",
    "year": 2018,
    "runtime": "134 mins",
    "genres": [
            "action",
            "adventure",
            "sci-fi"
    ]
}

Executing the SQL query

Let’s start in our database model again, and edit the Update() method to execute the following SQL query:

UPDATE movies 
SET title = $1, year = $2, runtime = $3, genres = $4, version = version + 1
WHERE id = $5
RETURNING version

Notice here that we’re incrementing the version value as part of the query. And then at the end we’re using the RETURNING clause to return this new, incremented, version value.

Like before, this query returns a single row of data so we’ll also need to use Go’s QueryRow() method to execute it. If you’re following along, head back to your internal/data/movies.go file and fill in the Update() method like so:

File: internal/data/movies.go
package data

...

func (m MovieModel) Update(movie *Movie) error {
    // Declare the SQL query for updating the record and returning the new version
    // number.
    query := `
        UPDATE movies 
        SET title = $1, year = $2, runtime = $3, genres = $4, version = version + 1
        WHERE id = $5
        RETURNING version`

    // Create an args slice containing the values for the placeholder parameters.
    args := []any{
        movie.Title,
        movie.Year,
        movie.Runtime,
        pq.Array(movie.Genres),
        movie.ID,
    }

    // Use the QueryRow() method to execute the query, passing in the args slice as a
    // variadic parameter and scanning the new version value into the movie struct.
    return m.DB.QueryRow(query, args...).Scan(&movie.Version)
}

...

It’s important to emphasize that — just like our Insert() method — the Update() method takes a pointer to a Movie struct as the input parameter and mutates it in-place again — this time updating it with the new version number only.

Creating the API handler

Now let’s head back to our cmd/api/movies.go file and update it to include the brand-new updateMovieHandler method.

Method URL Pattern Handler Action
PUT /v1/movies/:id updateMovieHandler Update the details of a specific movie

The nice thing about this handler is that the groundwork is already in place — our job here really just involves linking up the code and helper functions that we’ve already written to handle the request.

Specifically, we’ll need to:

  1. Extract the movie ID from the URL using the app.readIDParam() helper.
  2. Fetch the corresponding movie record from the database using the Get() method that we made in the previous chapter.
  3. Read the JSON request body containing the updated movie data into an input struct.
  4. Copy the data across from the input struct to the movie record.
  5. Check that the updated movie record is valid using the data.ValidateMovie() function.
  6. Call the Update() method to store the updated movie record in our database.
  7. Write the updated movie data in a JSON response using the app.writeJSON() helper.

So let’s go ahead and do exactly that:

File: cmd/api/movies.go
package main

...

func (app *application) updateMovieHandler(w http.ResponseWriter, r *http.Request) {
    // Extract the movie ID from the URL.
    id, err := app.readIDParam(r)
    if err != nil {
        app.notFoundResponse(w, r)
        return
    }

    // Fetch the existing movie record from the database, sending a 404 Not Found 
    // response to the client if we couldn't find a matching record.
    movie, err := app.models.Movies.Get(id)
    if err != nil {
        switch {
        case errors.Is(err, data.ErrRecordNotFound):
            app.notFoundResponse(w, r)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }

    // Declare an input struct to hold the expected data from the client.
    var input struct {
        Title   string       `json:"title"`
        Year    int32        `json:"year"`
        Runtime data.Runtime `json:"runtime"`
        Genres  []string     `json:"genres"`
    }

    // Read the JSON request body data into the input struct.
    err = app.readJSON(w, r, &input)
    if err != nil {
        app.badRequestResponse(w, r, err)
        return
    }

    // Copy the values from the request body to the appropriate fields of the movie
    // record.
    movie.Title = input.Title
    movie.Year = input.Year
    movie.Runtime = input.Runtime
    movie.Genres = input.Genres

    // Validate the updated movie record, sending the client a 422 Unprocessable Entity
    // response if any checks fail.
    v := validator.New()

    if data.ValidateMovie(v, movie); !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }

    // Pass the updated movie record to our new Update() method.
    err = app.models.Movies.Update(movie)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    // Write the updated movie record in a JSON response.
    err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil)
    if err != nil {
        app.serverErrorResponse(w, r, err)
    }
}

Lastly, to finish this off, we also need to update our application routes to include the new endpoint. Like so:

File: cmd/api/routes.go
package main

...

func (app *application) routes() http.Handler {
    router := httprouter.New()

    router.NotFound = http.HandlerFunc(app.notFoundResponse)
    router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)

    router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
    router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
    router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
    // Add the route for the PUT /v1/movies/:id endpoint.
    router.HandlerFunc(http.MethodPut, "/v1/movies/:id", app.updateMovieHandler)

    return app.recoverPanic(router)
}

Using the new endpoint

And with that, we’re now ready to try this out!

To demonstrate, let’s continue with the example we gave at the start of this chapter and update our record for Black Panther to include the genre sci-fi. As a reminder, the record currently looks like this:

$ curl localhost:4000/v1/movies/2
{
    "movie": {
        "id": 2,
        "title": "Black Panther",
        "year": 2018,
        "runtime": "134 mins",
        "genres": [
            "action",
            "adventure"
        ],
        "version": 1
    }
}

To make the update to the genres field we can execute the following API call:

$ BODY='{"title":"Black Panther","year":2018,"runtime":"134 mins","genres":["sci-fi","action","adventure"]}'
$ curl -X PUT -d "$BODY" localhost:4000/v1/movies/2
{
    "movie": {
        "id": 2,
        "title": "Black Panther",
        "year": 2018,
        "runtime": "134 mins",
        "genres": [
            "sci-fi",
            "action",
            "adventure"
        ],
        "version": 2
    }
}

That’s looking great — we can see from the response that the movie genres have been updated to include "sci-fi", and the version number has been incremented to 2 like we would expect.

You can also verify that the change has been persisted by making a GET /v1/movies/2 request again, like so:

$ curl localhost:4000/v1/movies/2
{
    "movie": {
        "id": 2,
        "title": "Black Panther",
        "year": 2018,
        "runtime": "134 mins",
        "genres": [
            "sci-fi",
            "action",
            "adventure"
        ],
        "version": 2
    }
}