Creating GET and POST Endpoints
Handle path parameters, query strings, and request body parsing.
Creating GET and POST Endpoints is a free R Academy lesson on CoddyKit. This is lesson 2 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, and your progress syncs across the web and the CoddyKit app. The R Academy course includes 4 lessons in total.
What Is Plumber?
plumber turns ordinary R functions into HTTP API endpoints using special comment annotations. Annotate a function with #* @get /path and Plumber creates a GET route that calls that function and returns its result as JSON.
Install with install.packages('plumber').
Your First GET Endpoint
A basic Plumber API lives in a file (e.g., api.R). Annotate the function with #* @get followed by the path. Plumber serializes the function's return value to JSON automatically.
# api.R
# library(plumber)
#
# #* Return a greeting
# #* @get /hello
# function() {
# list(message = 'Hello from Plumber!')
# }
#
# Start with:
# pr <- plumb('api.R')
# pr$run(port = 8000)Path Parameters with Type Hints
Embed variable path segments using angle bracket syntax: /users/<id:int>. Plumber parses the segment and passes it as a typed argument to your function. Supported types include int, dbl, and chr.
# #* Get a user by ID
# #* @get /users/<id:int>
# function(id) {
# # id is already an integer
# list(
# user_id = id,
# name = paste('User', id)
# )
# }
#
# GET /users/42 => {"user_id":42, "name":"User 42"}Query Parameters with #* @param
Document query parameters with #* @param name Description. The parameter name must match the function argument name. Plumber reads it from the query string automatically — no manual parsing needed.
# #* Search users by name
# #* @param name The name to search for
# #* @param limit Maximum results to return
# #* @get /users/search
# function(name = '', limit = '10') {
# limit <- as.integer(limit)
# # query string: /users/search?name=Alice&limit=5
# list(query = name, max = limit)
# }Creating a POST Endpoint
Use #* @post /path for endpoints that receive a request body. The special req argument gives access to the raw request object. Plumber passes it automatically when the function argument is named req.
# #* Create a new user
# #* @post /users
# function(req) {
# body <- jsonlite::fromJSON(req$postBody)
# # body$name, body$email are now available
# list(
# status = 'created',
# user_id = sample(1000:9999, 1),
# name = body$name
# )
# }Parsing the Request Body
req$postBody contains the raw JSON string from the POST body. Parse it with jsonlite::fromJSON(req$postBody) to get a named R list. Always validate required fields before processing.
# #* @post /orders
# function(req, res) {
# body <- jsonlite::fromJSON(req$postBody)
# if (is.null(body$product_id)) {
# res$status <- 400L
# return(list(error = 'product_id is required'))
# }
# list(
# order_id = as.integer(Sys.time()),
# product_id = body$product_id,
# quantity = body$quantity %||% 1
# )
# }HTTP Status Codes with res$status
The res argument (also auto-injected by Plumber) lets you set the HTTP response status code. Set it before returning: res$status <- 404L. Common codes:
- 200 — OK (default)
- 201 — Created
- 400 — Bad Request
- 404 — Not Found
- 500 — Internal Server Error
# #* @get /items/<id:int>
# function(id, res) {
# items <- list(
# list(id=1, name='Widget'),
# list(id=2, name='Gadget')
# )
# found <- Filter(function(x) x$id == id, items)
# if (length(found) == 0) {
# res$status <- 404L
# return(list(error = paste('Item', id, 'not found')))
# }
# found[[1]]
# }Returning Named Lists as JSON
Plumber serializes R return values to JSON using jsonlite. Named lists become JSON objects; unnamed lists become JSON arrays. Return a named list for structured responses.
# Named list => JSON object
# list(id=1, name='Alice') => {"id":1, "name":"Alice"}
#
# Unnamed list => JSON array
# list(1, 2, 3) => [1, 2, 3]
#
# Nested structures work too:
# list(
# user = list(id=1, name='Alice'),
# orders = list(list(id=101), list(id=102))
# )
# => {"user":{"id":1,"name":"Alice"}, "orders":[{"id":101},{"id":102}]}The Plumber Router Object
Load an annotated R file with plumb('api.R') to create a Plumber router object. Call pr$run(port = 8000) to start the server. In production you typically call pr_run(pr, host='0.0.0.0', port=8000).
# Standard plumber startup in api_start.R:
# library(plumber)
# pr <- plumb('api.R')
# pr$run(port = 8000, host = '0.0.0.0')
#
# Or with pipe style:
# plumb('api.R') |> pr_run(port = 8000)
#
# Test with:
# curl http://localhost:8000/helloHandling Multiple HTTP Methods
A single path can support multiple methods by writing separate annotated functions. Plumber routes requests to the correct function based on the HTTP method used.
# #* List all products
# #* @get /products
# function() {
# list(products = list(list(id=1, name='Widget')))
# }
#
# #* Create a product
# #* @post /products
# function(req) {
# body <- jsonlite::fromJSON(req$postBody)
# list(created = TRUE, name = body$name)
# }Testing Your API Endpoints
Use curl from the terminal or httr2 from R to test endpoints while the server runs. httr2 lets you write reproducible tests alongside your API code.
# From terminal:
# curl http://localhost:8000/users/42
# curl -X POST http://localhost:8000/users \
# -H 'Content-Type: application/json' \
# -d '{"name":"Alice","email":"alice@example.com"}'
#
# From R:
# library(httr2)
# resp <- request('http://localhost:8000/users/42') |> req_perform()
# resp_body_json(resp)Quick Check: Path Parameters
How do you declare a path parameter named id that Plumber should parse as an integer?
GET and POST Endpoints Recap
Building REST endpoints with Plumber:
#* @get /pathcreates a GET route;#* @post /pathcreates a POST route- Path params use
<name:type>syntax (int,dbl,chr) - Query params are auto-parsed into matching function arguments
- POST body is available via
jsonlite::fromJSON(req$postBody) - Set
res$statusfor non-200 HTTP responses - Return named lists — they serialize to JSON objects automatically
Learn R with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 43
- Lessons
- 159
Frequently Asked Questions
Is the “Creating GET and POST Endpoints” lesson free?
Yes — any 3 lessons of the R Academy learning path, including “Creating GET and POST Endpoints”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The R Academy course includes 4 lessons in total.
What will I learn in “Creating GET and POST Endpoints”?
Handle path parameters, query strings, and request body parsing. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start R Academy?
No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 2 of 4.
How long does the “Creating GET and POST Endpoints” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this R Academy lesson?
Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Introduction to Plumber and REST
- Creating GET and POST Endpoints
- Authentication and API Security
- Deploying Plumber APIs to Production