Run app
cd
into the11_csv_xlsx_upload/
dir:cd 11_csv_xlsx_upload/
Fire up R:
R
Restore package dependencies:
::restore() renv
Once done, exit R.
server.R
is the entry point. To start the API, run this on the terminal:Rscript server.R
Explanation
Let’s first import the functions we’ll be using:
::use(
box
readxl[read_xlsx],
data.table[fread],
ambiorix[Ambiorix],
webutils[parse_http],
cli[cli_alert_danger], )
I’ve come to love {box}
because it forces me to be explicit about my imports. I almost always know from which package/module a function/object is from.
Skeleton
Next, let’s define the API skeleton:
1<- Ambiorix$new(port = 3000, host = "127.0.0.1")
app 2$limit <- 35 * 1024 * 1024
app
$
app3set_error(error_handler)$
4get("/", home_get)$
5post("/csv", csv_upload_post)$
6post("/xlsx", xlsx_upload_post)$
7start()
- 1
-
Initialize a new ambiorix app. Set
port
&host
to run app on. - 2
- Increase max body size to 35 MB. Think of this as the max file upload size for now.
- 3
- Set an error handler for the API.
- 4
-
Add handler for requests at
/
. - 5
-
Add handler for requests at
/csv
. - 6
-
Add handler for requests at
/xlsx
. - 7
- Start API.
Now, let’s create the handlers.
Error handler
I’ve made it a habit to always have an error handler for my APIs. Ideally, this is where you log any server errors that occurred, either due to failed operations or bugs.
#' 500 error handler middleware
#'
#' @param req Request object.
#' @param res Response object.
#' @param error Error object. See [stop()].
#' @return `res$json()`
#' @export
<- \(req, res, error = NULL) {
error_handler if (!is.null(error)) {
<- conditionMessage(error)
msg cli_alert_danger(text = msg)
}
<- list(
response code = 500L,
msg = "A server error occurred!"
)
$set_status(500L)$json(response)
res }
Hello, World!
Are you even a real programmer if you don’t always start with a “Hello, World”?
#' Handle GET at '/'
#'
#' @param req Request object.
#' @param res Response object.
#' @return `res$json()`
#' @export
<- \(req, res) {
home_get <- list(
response code = 200L,
msg = "hello, world!"
)
$json(response)
res }
csv upload
We’re finally at the juicy part of this post!
#' Handle POST at '/csv'
#'
#' @param req Request object.
#' @param res Response object.
#' @return `res$json()`
#' @export
<- \(req, res) {
csv_upload_post 1<- req$rook.input$read()
body
2<- list(
response_400 code = 400L,
msg = "please upload a csv file with the key 'file' in the request body"
)
# if the req body is empty, return a 400:
3<- length(body) == 0L
empty if (empty) {
return(
$set_status(400L)$json(response_400)
res
)
}
4<- parse_http(
postdata body = body,
content_type = req$CONTENT_TYPE
)
5<- postdata$file
file_details
# check 'content_type' of file:
6<- identical(
ok x = file_details$content_type,
y = "text/csv"
)
if (!ok) {
return(
$set_status(400L)$json(response_400)
res
)
}
# write file temporarily:
7<- tempfile(fileext = ".csv")
temp on.exit(unlink(x = temp))
writeBin(object = file_details$value, con = temp)
# read file:
8<- fread(file = temp)
x 9print(x)
10<- list(
response code = 200L,
msg = "file uploaded!"
)
$json(response)
res }
- 1
- Read the request body.
- 2
-
Create a bad request response:
response_400
. - 3
-
If the request body is empty, return
response_400
. - 4
- Parse the body into a named list of key-value pairs. POST request variables are stored as key-value pairs in the request body.
- 5
-
Get the variable/field named
file
from the list. - 6
-
If the content type of the file is not
text/csv
, returnresponse_400
. To learn about common mime types, refer to MIME_types. - 7
- Temporarily write the file to disk.
- 8
- Read the file.
- 9
-
Print the
data.table
. - 10
- Return a 200 success response.
xlsx upload
This is almost identical to how we’ve handled the csv file upload, except that we are now working with xlsx files.
#' Handle POST at '/xlsx'
#'
#' @param req Request object.
#' @param res Response object.
#' @return `res$json()`
#' @export
<- \(req, res) {
xlsx_upload_post <- req$rook.input$read()
body
<- list(
response_400 code = 400L,
msg = "please upload an xlsx file with the key 'file' in the request body"
)
# if the req body is empty, return a 400:
<- length(body) == 0L
empty if (empty) {
return(
$set_status(400L)$json(response_400)
res
)
}
<- parse_http(
postdata body = body,
content_type = req$CONTENT_TYPE
)
<- postdata$file
file_details
# check 'content_type' of file:
<- identical(
ok x = file_details$content_type,
y = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
if (!ok) {
return(
$set_status(400L)$json(response_400)
res
)
}
# write file temporarily:
<- tempfile(fileext = ".xlsx")
temp on.exit(unlink(x = temp))
writeBin(object = file_details$value, con = temp)
# read file:
<- read_xlsx(path = temp)
x print(x)
<- list(
response code = 200L,
msg = "file uploaded!"
)
$json(response)
res }
Test
This example comes with:
iris.csv
: A csv test file.iris.xlsx
: An xlsx test file.example.R
: An R script showing how you can make requests to the API using R.
To make requests to the API, you can either use postman or open another R session in the same working directory as this example and run example.R
:
::use(
box
curl[form_file],
httr2[
request,
req_perform,
req_url_path,
last_response,
resp_body_json,
req_body_multipart,
]
)
<- "http://127.0.0.1:3000"
base_url <- form_file(
file path = "iris.csv",
type = "text/csv",
name = "iris.csv"
)
<- request(base_url = base_url) |>
req req_url_path("/csv") |>
req_body_multipart(file = file)
# use `tryCatch()` in case an error occurs while performing the request:
tryCatch(
expr = req |>
req_perform() |>
resp_body_json(),
error = \(e) {
print("An error occurred!")
<- last_response() |> resp_body_json()
error print(error)
} )
::use(
box
curl[form_file],
httr2[
request,
req_perform,
req_url_path,
last_response,
resp_body_json,
req_body_multipart,
]
)
<- "http://127.0.0.1:3000"
base_url <- form_file(
file path = "iris.xlsx",
type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
name = "iris.xlsx"
)
<- request(base_url = base_url) |>
req req_url_path("/xlsx") |>
req_body_multipart(file = file)
# use `tryCatch()` in case an error occurs while performing the request:
tryCatch(
expr = req |>
req_perform() |>
resp_body_json(),
error = \(e) {
print("An error occurred!")
<- last_response() |> resp_body_json()
error print(error)
} )