09: goals

A CRUD application backend

09_goals
Author

Kennedy Mwavu

Published

July 10, 2024

Prerequisites

Run app

  1. cd into the 09_goals/ dir:

    cd 09_goals/
  2. Fire up R:

    R
  3. Restore package dependencies:

    renv::restore()

    Once done, exit R.

  4. server.R is the entry point. To start the app, run this on the terminal:

    Rscript server.R

Explanation

This app starts a server and listens on port 5000 for connections.

In this example, we build a CRUD application backend: Goals.

Here are the defined routes:

  • /api:
    • /users:
      • POST /: Register new user
      • POST /login: Login user
      • GET /me: Get user data
      • PUT /me: Update user details
      • DELETE /me: Delete user account
    • /goals:
      • GET /: Get all user goals
      • POST /: Create a goal
      • PUT /:id: Update a goal
      • DELETE /:id: Delete a goal

You will be able to Create, Read, Update & Delete Goals.

Here’s what’s covered:

  • Ambiorix + MongoDB
  • Working with middleware:
    • Auth middleware: You will learn how you can use JSON Web Tokens (JWT) to protect routes
    • Error handling middleware

Requests to the API

Let’s explore how you can send requests to the API. We’ll do so from another R session.

Be sure to import the required functions first:

box::use(
  httr2[
    request,
    req_method,
    req_perform,
    req_url_path,
    last_response,
    resp_body_json,
    req_body_multipart,
    req_auth_bearer_token,
  ]
)

/api/users*

Since the API requires JWT auth, you first need to create an account. To do that, make a POST request to /api/users:

base_url <- "http://127.0.0.1:5000"

# registration details:
user_details <- list(
  name = "mwavu",
  email = "mwavu@mail.com",
  password = "test123"
)

req <- request(base_url = base_url) |>
  req_url_path("/api/users") |>
  req_body_multipart(!!!user_details)

# use `tryCatch()` in case an error occurs while performing the request:
res <- tryCatch(
  expr = req |>
    req_perform() |>
    resp_body_json(),
  error = \(e) {
    print("An error occurred!")
    error <- last_response() |> resp_body_json()
    print(error)
  }
)

If that’s successful, you get back a named list of 3:

  • code
  • msg
  • user:
    • _id: User id
    • name
    • email
    • token: A JWT token

Here is an example:

str(res)
# List of 3
#  $ code: int 201
#  $ msg : chr "Success."
#  $ user:List of 4
#   ..$ _id  : chr "669ecec3f555b0571b09a3e1"
#   ..$ name : chr "mwavu"
#   ..$ email: chr "mwavu@mail.com"
#   ..$ token: chr "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjQzNjIwNTIsImlhdCI6MTcyMTY4MzY1MiwidXNlcl9pZCI6IjY2OWVjZWMzZj"| __truncated__

To login a user, we make a POST request to /api/users/login:

base_url <- "http://127.0.0.1:5000"

# login details:
user_details <- list(
  email = "mwavu@mail.com",
  password = "test123"
)

req <- request(base_url = base_url) |>
  req_url_path("/api/users/login") |>
  req_body_multipart(!!!user_details)

# use `tryCatch()` in case an error occurs while performing the request:
res <- tryCatch(
  expr = req |>
    req_perform() |>
    resp_body_json(),
  error = \(e) {
    print("An error occurred!")
    error <- last_response() |> resp_body_json()
    print(error)
  }
)

Again, on successful login, you get back a named list of 3:

  • code
  • msg
  • user:
    • _id
    • name
    • email
    • token: A JWT token
str(res)
# List of 3
#  $ code: int 200
#  $ msg : chr "Success."
#  $ user:List of 4
#   ..$ _id  : chr "669ecec3f555b0571b09a3e1"
#   ..$ name : chr "mwavu"
#   ..$ email: chr "mwavu@mail.com"
#   ..$ token: chr "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjQzNjIzMjksImlhdCI6MTcyMTY4MzkyOSwidXNlcl9pZCI6IjY2OWVjZWMzZj"| __truncated__

To get details of a specific user, you need the JWT token returned during register or login. The token is verified by the auth middleware.

Make a GET request to /api/users/me and include the JWT as an auth bearer token:

base_url <- "http://127.0.0.1:5000"

# the JWT token from signup/login:
token <- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjQzNjIzMjksImlhdCI6MTcyMTY4MzkyOSwidXNlcl9pZCI6IjY2OWVjZWMzZjU1NWIwNTcxYjA5YTNlMSJ9.pzuhMl49qoXRKrUyLvTNHNRkO9bxMgSZd8dEeJR-adM"

req <- request(base_url = base_url) |>
  req_url_path("/api/users/me") |>
  req_auth_bearer_token(token = token)

# use `tryCatch()` in case an error occurs while performing the request:
res <- tryCatch(
  expr = req |>
    req_perform() |>
    resp_body_json(),
  error = \(e) {
    print("An error occurred!")
    error <- last_response() |> resp_body_json()
    print(error)
  }
)

If successful, that returns list containing a named list of 3:

  • code
  • msg
  • user:
    • _id
    • name
    • email

For example:

str(res)
# List of 3
#  $ code: int 200
#  $ msg : chr "Success."
#  $ user:List of 3
#   ..$ _id  : chr "669ecec3f555b0571b09a3e1"
#   ..$ name : chr "mwavu"
#   ..$ email: chr "mwavu@mail.com"

To update user details (name, email, password), send a PUT request to /api/users/me with the new details in the body of the request:

base_url <- "http://127.0.0.1:5000"

# the JWT token from signup/login:
token <- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjQyNjE1MDUsImlhdCI6MTcyMTU4MzEwNSwidXNlcl9pZCI6IjY2OWQ0NjAxYzdhZjI5MmMxNjA4YzIzMSJ9.C9ooaUJz-eZWJ69SSpbjGQXc1Mprd9hLm58vt3z6Ons"

# you can update the name, email or password:
new_details <- list(
  name = "mwavukennedy",
  password = "test1234"
)

req <- request(base_url = base_url) |>
  req_url_path("/api/users/me") |>
  req_auth_bearer_token(token = token) |>
  req_method(method = "PUT") |>
  req_body_multipart(!!!new_details)

# use `tryCatch()` in case an error occurs while performing the request:
res <- tryCatch(
  expr = req |>
    req_perform() |>
    resp_body_json(),
  error = \(e) {
    print("An error occurred!")
    error <- last_response() |> resp_body_json()
    print(error)
  }
)

If successful, you should get back details of the updated user:

str(res)
# List of 3
#  $ code: int 200
#  $ msg : chr "Updated successfully!"
#  $ user:List of 1
#   ..$ :List of 3
#   .. ..$ _id  : chr "669ecec3f555b0571b09a3e1"
#   .. ..$ name : chr "mwavukennedy"
#   .. ..$ email: chr "mwavu@mail.com"

If you change the email/password and try logging in using the old credentials, note that the login attempt will fail.

To delete a user account, send a DELETE request to /api/users/me:

base_url <- "http://127.0.0.1:5000"

# the JWT token from signup/login:
token <- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjQyNjE1MDUsImlhdCI6MTcyMTU4MzEwNSwidXNlcl9pZCI6IjY2OWQ0NjAxYzdhZjI5MmMxNjA4YzIzMSJ9.C9ooaUJz-eZWJ69SSpbjGQXc1Mprd9hLm58vt3z6Ons"

req <- request(base_url = base_url) |>
  req_url_path("/api/users/me") |>
  req_auth_bearer_token(token = token) |>
  req_method(method = "DELETE")

# use `tryCatch()` in case an error occurs while performing the request:
res <- tryCatch(
  expr = req |>
    req_perform() |>
    resp_body_json(),
  error = \(e) {
    print("An error occurred!")
    error <- last_response() |> resp_body_json()
    print(error)
  }
)

If successful, you get back a 200 and a message:

str(res)
# List of 2
#  $ code: int 200
#  $ msg : chr "Account deleted"

/api/goals*

Every route in /api/goals* is protected, meaning they can only be accessed by an authenticated user. Also, each user only has access to the goals they set, not anyone elses.

In other words, send the JWT as an auth bearer token in your requests.

Let’s set a goal by sending a POST request to /api/goals:

base_url <- "http://127.0.0.1:5000"

# the JWT token from signup/login:
token <- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjQyMDY5NDcsImlhdCI6MTcyMTUyODU0NywidXNlcl9pZCI6IjY2OWM2ZWU0ZGRmYTdiZTZhMTBmMDdlMSJ9.rpSOL0LynYm2BBP60Ikpz-GNIY6mR_ZKKzH9Tai2IS4"

# your goal:
text <- "Learn Rust"

req <- request(base_url = base_url) |>
  req_url_path("/api/goals") |>
  req_auth_bearer_token(token = token) |>
  req_body_multipart(text = text)

# use `tryCatch()` in case an error occurs while performing the request:
res <- tryCatch(
  expr = req |>
    req_perform() |>
    resp_body_json(),
  error = \(e) {
    print("An error occurred!")
    error <- last_response() |> resp_body_json()
    print(error)
  }
)

If successful, that should return a list of 3:

  • code
  • msg
  • goal:
    • _id: goal id
    • user_id: user id
    • text: the goal

For example:

str(res)
# List of 3
#  $ code: int 201
#  $ msg : chr "Success."
#  $ goal:List of 3
#   ..$ _id    : chr "669ed1ce24f7bd52b80c6e92"
#   ..$ user_id: chr "669ed19924f7bd52b80c6e91"
#   ..$ text   : chr "Learn Rust"

To get all goals a user has set, send a GET request to /api/goals:

base_url <- "http://127.0.0.1:5000"

# the JWT token from signup/login:
token <- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjQyMDY5NDcsImlhdCI6MTcyMTUyODU0NywidXNlcl9pZCI6IjY2OWM2ZWU0ZGRmYTdiZTZhMTBmMDdlMSJ9.rpSOL0LynYm2BBP60Ikpz-GNIY6mR_ZKKzH9Tai2IS4"

req <- request(base_url = base_url) |>
  req_url_path("/api/goals") |>
  req_auth_bearer_token(token = token)

# use `tryCatch()` in case an error occurs while performing the request:
res <- tryCatch(
  expr = req |>
    req_perform() |>
    resp_body_json(),
  error = \(e) {
    print("An error occurred!")
    error <- last_response() |> resp_body_json()
    print(error)
  }
)

If successful, that should return a list of named lists. Each of the nested lists has 2 items:

  • _id: Id of the goal
  • text: The goal

For example:

str(res)
# List of 1
#  $ goals:List of 3
#   ..$ :List of 2
#   .. ..$ _id : chr "669ed1ce24f7bd52b80c6e92"
#   .. ..$ text: chr "Learn Rust"
#   ..$ :List of 2
#   .. ..$ _id : chr "669ed27324f7bd52b80c6e93"
#   .. ..$ text: chr "Call Mum"
#   ..$ :List of 2
#   .. ..$ _id : chr "669ed27c24f7bd52b80c6e94"
#   .. ..$ text: chr "Visit Aunt"

To update a goal, send a PUT request to /api/users/:id:

base_url <- "http://127.0.0.1:5000"

# the JWT token from signup/login:
token <- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjQyMDY5NDcsImlhdCI6MTcyMTUyODU0NywidXNlcl9pZCI6IjY2OWM2ZWU0ZGRmYTdiZTZhMTBmMDdlMSJ9.rpSOL0LynYm2BBP60Ikpz-GNIY6mR_ZKKzH9Tai2IS4"

updated_goal <- "Learn Rust & Postgres"

req <- request(base_url = base_url) |>
  req_url_path("/api/goals/669ed1ce24f7bd52b80c6e92") |>
  req_auth_bearer_token(token = token) |>
  req_method(method = "PUT") |>
  req_body_multipart(text = updated_goal)

# use `tryCatch()` in case an error occurs while performing the request:
res <- tryCatch(
  expr = req |>
    req_perform() |>
    resp_body_json(),
  error = \(e) {
    print("An error occurred!")
    error <- last_response() |> resp_body_json()
    print(error)
  }
)

If successful, you should get back a named list of 3:

  • code
  • msg
  • goal:
    • _id
    • text
str(res)
# List of 3
#  $ code: int 200
#  $ msg : chr "Goal updated successfully"
#  $ goal:List of 2
#   ..$ _id : chr "669ed1ce24f7bd52b80c6e92"
#   ..$ text: chr "Learn Rust & Postgres"

To delete a goal, send a, well, DELETE request to /api/goals/:id:

base_url <- "http://127.0.0.1:5000"

# the JWT token from signup/login:
token <- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjQyMDY5NDcsImlhdCI6MTcyMTUyODU0NywidXNlcl9pZCI6IjY2OWM2ZWU0ZGRmYTdiZTZhMTBmMDdlMSJ9.rpSOL0LynYm2BBP60Ikpz-GNIY6mR_ZKKzH9Tai2IS4"

req <- request(base_url = base_url) |>
  req_url_path("/api/goals/669ed1ce24f7bd52b80c6e92") |>
  req_auth_bearer_token(token = token) |>
  req_method(method = "DELETE")

# use `tryCatch()` in case an error occurs while performing the request:
res <- tryCatch(
  expr = req |>
    req_perform() |>
    resp_body_json(),
  error = \(e) {
    print("An error occurred!")
    error <- last_response() |> resp_body_json()
    print(error)
  }
)

If successful, you will again get back a named list of 3:

  • code
  • msg
  • goal:
    • _id
    • text
str(res)
# List of 3
#  $ code: int 200
#  $ msg : chr "Goal deleted successfully"
#  $ goal:List of 2
#   ..$ _id : chr "669ed1ce24f7bd52b80c6e92"
#   ..$ text: chr "Learn Rust & Postgres"

Live reloading

See how you can enable ✨live reloading✨.