We just went through so much and we only created two endpoints in our API! However, reviewing all those basics was fundamental for our progress and what we will be learning next. Don’t get too excited yet - we still have a few things to learn about HTTP.
One of the most underrated features of HTTP is its methods. Why underrated? Because browsers, the most used web API clients in the world, and the HTML
format, the most used web format in the world, don’t make full use of them.
We’ve seen before that websites are just web APIs with a web browser as client, outputting HTML
instead of JSON
or XML
. Still, we have two pieces of software talking to each other, a web browser and a web server. Sadly, HTML
only supports two HTTP methods: GET
and POST
. You already know GET
, as we’ve used it a few times since the beginning of this module. But do you really know what HTTP methods are? Do you know what idempotent means or why some methods are classified as ‘safe’?
HTTP methods, also known as HTTP verbs, are just English verbs like GET
which define what action the client wants to be performed on the identified resource. Each method defines exactly what the client wants to happen when sent to a server, but as always, it all depends on the server. If a server implementation doesn’t follow the HTTP RFC recommendations, you will need to study the documentation for this server application and adapt. That’s why following web standards when you build a web API is so important, you don’t want to leave your users having to figure out how everything works in your specific case, instead of just resorting to their general knowledge of HTTP.
In the original specification of HTTP (version 1.0), only three of those methods were defined: GET
, POST
and HEAD
. Five new verbs were added to the revision of HTTP (version 1.1): OPTIONS
, PUT
, DELETE
, TRACE
and CONNECT
. The RFC 5789 extends HTTP with a new method: PATCH
. This adds up to nine different HTTP methods, but we will only use half of them most of the time.
When working with a web API, you sometimes need to create or destroy some data. Other times, you just need to retrieve some data without being destructive.
That’s exactly why some methods are considered “safe”; they should not have any side effects and should only retrieve data. You can, of course, implement something in your web API when a safe method is called, like updating that user’s quota for example. However, the client cannot be held responsible for those modifications since he did not request them and considered the method safe to use.
The only safe methods are GET
and HEAD
.
For example, if I run the request GET /users
in our Sinatra API, there is no side effect. I will just keep getting a list of users and I can run it as many times as I want. This leads me to idempotence.
Idempotence is a funky word; I had no idea what it meant until I stumbled upon it in the HTTP RFC document. Idempotence is the property of some operations in mathematics and computer science, where running the same operation multiple times will not change the result after the initial run. It has the same meaning in the HTTP context.
The impact of sending 10 HTTP requests with an idempotent method is the same as sending just one request.
The idempotent methods are GET
, HEAD
, PUT
, DELETE
, OPTIONS
and TRACE
.
If I send the same GET
request multiple times, I should just get the same representation, over and over again. There are some situations that can remove the idempotence property from a method, like when an error arises or the user’s quota is full. We need to keep a pragmatic mindset towards idempotence when building web APIs.
It’s the same with DELETE
, which shouldn’t raise an error when a client tries to delete a resource that has already been deleted. However, I’m not sure that’s the best thing to do since, from a pragmatic standpoint, you’d like to let the client know that it was already deleted and it can stop sending the same request.
We already used GET
in our little API. Now that we’ve learned that there are more methods, it’s time to implement them!
GET
is safe and idempotent. This method is meant to retrieve information from the specified resource in the form of a representation.
The GET
method can also be used as a “conditional GET
” when conditional header fields are used and as ‘partial GET
’ when the Range
header field is used.
As a reminder, here is our GET
endpoint for the users resource.
get '/users' do
type = accepted_media_type
if type == 'json'
content_type 'application/json'
users.map { |name, data| data.merge(id: name) }.to_json
elsif type == 'xml'
content_type 'application/xml'
Gyoku.xml(users: users)
end
end
We are also going to need a way to retrieve specific users; to do this, we need to have one URI available for each user. Each user will be a resource and we will get representations for that user specifically. Luckily, we don’t have to hardcode each URI as /users/thibault
, /users/john
and so on, that would be a pain to do! We can just use a match URI like /users/:first_name
.
Below is the code for this new endpoint.
# webapi.rb
# ...
# get '/users'
get '/users/:first_name' do |first_name|
type = accepted_media_type
if type == 'json'
content_type 'application/json'
users[first_name.to_sym].merge(id: first_name).to_json
elsif type == 'xml'
content_type 'application/xml'
Gyoku.xml(first_name => users[first_name.to_sym])
end
end
It is very similar to get '/users'
, the main difference being the data that we send back to the client. Here we select the wanted user from our data hash and send it back to the client after serializing it. For JSON
, we use users[first_name].merge(id: first_name).to_json
and for XML
Gyoku.xml(first_name => users[first_name])
.
Let’s do one curl
request for each available format. First, stop your server if it’s running and restart it with ruby webapi.rb
.
With JSON
, the default format:
curl http://localhost:4567/users/thibault
Output
{"first_name":"Thibault", "last_name":"Denizet", "age":25, "id":"thibault"}
With XML
:
curl -i http://localhost:4567/users/thibault \
-H "Accept: application/xml"
Output
<thibault>
<firstName>Thibault</firstName><lastName>Denizet</lastName><age>25</age>
</thibault>
Looks good! Let’s proceed.
Don’t try to query for users that don’t exist yet; we are not handling errors yet! There will be a full section about that in the next chapter.
The HEAD
method works in pretty much the same way as the GET
method. The only difference is that the server is not supposed to return a body after receiving a HEAD
request. The HTTP headers received back by the client should be exactly like the ones it would receive from a GET
request.
HEAD
is safe and idempotent. It can be used to test a URI before actually sending a request to retrieve data.
Let’s add support for this method in our web API. We can first duplicate the get '/users'
route. There are only two changes to make after that. Changing get
to head
and preventing anybody from being sent, by removing:
users.map { |name, data| data.merge(id: name) }.to_json
And:
Gyoku.xml(users: users)
We end up with the following, which will send back the same headers as a GET
request, minus the body:
# webapi.rb
# ...
# get '/users'
# get '/users/:first_name'
head '/users' do
type = accepted_media_type
if type == 'json'
content_type 'application/json'
elsif type == 'xml'
content_type 'application/xml'
end
end
Restart your server after adding this route.
To run a HEAD
request with curl
, we need to use the -I
option. Simply using the -X
option and setting it to HEAD
(-X HEAD
) will correctly send a HEAD
request, but it will then wait for data to be received. Let’s also use the verbose option (-v
) to see if the sent request meets our expectations.
curl -I -v http://localhost:4567/users
Here’s the request sent…
HEAD /users HTTP/1.1
Host: localhost:4567
User-Agent: curl/7.43.0
Accept: */*
and the response received:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 162
X-Content-Type-Options: nosniff
Connection: keep-alive
Server: thin
All good! It seems we correctly followed the HTTP RFC to implement our HEAD
endpoint. The code is a big duplication of the GET
endpoint; don’t worry, we will refactor that once we have all our endpoints. For now, let’s focus on learning more about the remaining methods.
POST
is as famous as GET
because it’s the only other HTTP method supported by the HTML
format. Building web applications usually revolves around using either GET
to get some HTML
documents or POST
to post some form of data that will do anything from creating to updating to deleting entities.
Well, that doesn’t really follow what’s defined in the HTTP RFC, unfortunately. POST
is supposed to be used only to create new entities, be it as a new database record or as an annotation of an existing resource.
Let’s focus on the POST
method as a means to create a new record of the resource identified in the request. It is neither safe nor idempotent to create new records.
The RFC also specifies how to respond to the client by using specific status codes like 200
(OK), 204
(No Content) or 201
(Created). The first two codes should only be used if the created entity cannot be identified by a URI; 200
when the response contains a body and 204
when it does not. Otherwise, 201
should be returned with the Location
header set to the URI of this new entity.
A best practice is to return the newly created entity as the body of a POST
request. It should only be done if the data of the entity that was created and the data that was sent with the request are different. For example, if a new field like created_at
was added during the creation, then sending back the entity right away makes sense, and saves the client from making another call with the URI present in the Location
header. In our Sinatra API, we will be returning 201
without a body, but when we work on our next API we will start sending back entities right away.
You can also decide to send back
Let’s add a POST
endpoint to create new users in our API. Since we don’t persist anything, we will just add the sent user into the users
hash. We are not going to handle any error for now, so if you send something that’s not a valid JSON
, it will crash.
Don’t worry - we’ll add error handling very soon.
You can find the code for this endpoint below. In the code, we first get the payload of the request with request.body.read
that we then parse to get a Ruby hash from. Then we store the received user in our users hash using the user first_name
as key. Finally, we send back 201
to the client to confirm the creation.
# webapi.rb
# ...
# get '/users'
# get '/users/:first_name'
# head '/users'
post '/users' do
user = JSON.parse(request.body.read)
users[user['first_name'].downcase.to_sym] = user
url = "http://localhost:4567/users/#{user['first_name']}"
response.headers['Location'] = url
status 201
end
Restart your server after adding this route.
Let’s give it a try with curl
.
curl -X POST -v http://localhost:4567/users \
-H "Content-Type: application/json" \
-d '{"first_name":"Samuel","last_name":"Da Costa","age":19}'
Request Sent:
POST /users HTTP/1.1
Host: localhost:4567
User-Agent: curl/7.43.0
Accept: */*
Content-Type: application/json
Content-Length: 55
.. {JSON data} ...
Response Received:
HTTP/1.1 201 Created
Content-Type: text/html;charset=utf-8
Location: http://localhost:4567/users/Samuel
Content-Length: 0
... Hidden Headers ...
Awesome, everything seems to be working fine! We can double-check by getting our list of users.
curl http://localhost:4567/users
[
{"first_name":"Thibault", "last_name":"Denizet", "age":25},
{"first_name":"Simon", "last_name":"Random", "age":26},
{"first_name":"John", "last_name":"Smith", "age":28},
{"first_name":"Samuel", "last_name":"Da Costa", "age":19}
]
Great, Samuel is there!
No Samuel was harmed in the making of this HTTP request.
PUT
, just like POST
, is not safe; however it is idempotent. This might surprise you since PUT
is often used to send entity updates to the server, as in Rails before Rails 4 was released.
The truth is that PUT
can be used by a client to tell the server to store the given entity at the specified URI. This will not just update it, but will completely replace the entity available at that URI with the one supplied by the client.
PUT
can also be used to create a new entity at the specified URI if there is no entity identified by this URI yet. In such a case, the server should create it.
In most situations, the main difference between PUT
and POST
is that POST
uses a URI like /users
, which identifies the users resource, whereas PUT
works with a URI such as /users/1
, which identifies one specific user as a resource.
It would be totally possible to send a PUT /users
request with a list of users to replace all the users currently stored. However, in most scenarios, it doesn’t really make sense to do this kind of thing.
Let’s implement it in our web API. The code looks similar to the POST
endpoint, but in this case, we don’t need to set the Location
header (the client is already calling that user endpoint with /users/thibault
).
Once again, we are returning 204 No Content
or 201 Created
and not 200 OK
because we are not sending back any representation of the resource since the client already has the same data for that entity.
# webapi.rb
# ...
# get '/users'
# get '/users/:first_name'
# head '/users'
# post '/users'
put '/users/:first_name' do |first_name|
user = JSON.parse(request.body.read)
existing = users[first_name.to_sym]
users[first_name.to_sym] = user
status existing ? 204 : 201
end
Restart your server after adding this route.
Notice how we either return 204 No Content
or 201 Created
, depending on the user
entity we received and whether it already exists or not, by defining the existing
variable. You should also understand that we completely replace all the data with what the client has sent instead of just updating it inside the users
hash.
Now it’s time to run some curl
requests to see if everything is working as expected. First, we are going to create a user at the URI /users/jane
.
curl -X PUT -v http://localhost:4567/users/jane \
-H "Content-Type: application/json" \
-d '{"first_name":"Jane","last_name":"Smiht","age":24}'
Request Sent:
PUT /users/Jane HTTP/1.1
Host: localhost:4567
User-Agent: curl/7.43.0
Accept: */*
Content-Type: application/json
Content-Length: 50
upload completely sent off: 50 out of 50 bytes
Response Received:
HTTP/1.1 201 Created
Content-Type: text/html;charset=utf-8
Content-Length: 0
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Connection: keep-alive
Server: thin
Great, it seems Jane was saved correctly. Let’s double-check that by accessing /users/Jane
.
curl http://localhost:4567/users/jane
Output
{"first_name":"Jane", "last_name":"Smiht", "age":24, "id":"jane"}
Wait! There is a typo in her name. Plus she is not 24, she is actually 25. We need to quickly fix this! Let’s send another request with the correct values.
curl -X PUT -v http://localhost:4567/users/jane \
-H "Content-Type: application/json" \
-d '{"first_name":"Jane","last_name":"Smith","age":25}'
Request Sent:
PUT /users/jane HTTP/1.1
Host: localhost:4567
User-Agent: curl/7.43.0
Accept: */*
Content-Type: application/json
Content-Length: 50
upload completely sent off: 50 out of 50 bytes
Response Received:
HTTP/1.1 204 No Content
X-Content-Type-Options: nosniff
Connection: close
Server: thin
Alright, looks good! We can double-check one last time by asking for a representation of the Jane resource.
curl http://localhost:4567/users/jane
Output
{"first_name": "Jane", "last_name":"Smith", "age":25, "id":"jane"}
Our PUT
endpoint is working well. It can either create a new entity if the entity does not already exist, or override all the values for an existing one.
PATCH
is a bit special. This method is not part of the HTTP RFC 2616, but was later defined in the RFC 5789 in order to do “partial resource modification.” It is basically a way for the client to change only some specific values for an entity instead of having to resend the whole entity data.
While PUT
only allows the complete replacement of a document, PATCH
allows the partial modification of a resource.
Just like PUT
, it uses a unique entity URI like /users/1
and the server can create a new entity if there is none that exists yet. However, in my opinion, it’s better to keep POST
or PUT
for creation and only use PATCH
for updates. That’s why in our web API, the PATCH
endpoint for a user resource will only be able to update its data.
It would also be possible to send PATCH
requests to a URI like /users
to do partial modifications to more than one user in one request.
By default, PATCH
is neither safe nor idempotent.
PATCH
requests can be made idempotent in order to avoid conflicts between multiple updates by using two headers, Etag
and If-Match
, to make conditional requests. These headers will contain a value to ensure that the client and the server have the same version of the entity and prevent an update if they have different versions. This is not required for every operation, only for the ones where a conflict is possible. For example, adding a line to a log file through a PATCH
request does not require checking for conflict.
We will learn how to avoid conflicts later, when we build our full-scale Rails API. For now, we both know we won’t create any conflicts running one request at a time with curl
. It’s so annoying having to copy/paste the serialization part, and I can’t wait for the refactoring that we will be doing soon.
The code for the PATCH
endpoint is a bit long, but we will be able to refactor it after we are done reviewing all the HTTP methods.
# webapi.rb
# ...
# get '/users'
# get '/users/:first_name'
# head '/users'
# post '/users'
# put '/users/:first_name'
patch '/users/:first_name' do |first_name|
type = accepted_media_type
user_client = JSON.parse(request.body.read)
user_server = users[first_name.to_sym]
user_client.each do |key, value|
user_server[key.to_sym] = value
end
if type == 'json'
content_type 'application/json'
user_server.merge(id: first_name).to_json
elsif type == 'xml'
content_type 'application/xml'
Gyoku.xml(first_name => user_server)
end
end
Restart your server after adding this route.
I’m going to be 26 soon, so let’s update my age in our super-fast memory database (a.k.a a Ruby hash).
curl -X PATCH -v http://localhost:4567/users/thibault \
-H "Content-Type: application/json" \
-d '{"age":26}'
Request Sent:
PATCH /users/thibault HTTP/1.1
Host: localhost:4567
User-Agent: curl/7.43.0
Accept: */*
Content-Type: application/json
Content-Length: 10
upload completely sent off: 10 out of 10 bytes
Response Received:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 56
X-Content-Type-Options: nosniff
Connection: keep-alive
Server: thin
{"first_name":"Thibault", "last_name":"Denizet", "age":26, "id":"thibault"}
We just confirmed that our PATCH
endpoint allows us to cherry-pick what we want to update on the thibault
resource.
Since we only send what we want to update in an atomic way, PATCH
requests are usually smaller in size than the PUT
ones. This can have a positive impact on the performances of web APIs.
The DELETE
method is used by a client to ask the server to delete the resource identified by the specified URI. The server should only tell the client that the operation was successful in case it’s really going to delete the resource or at least move it to an inaccessible location. The server should send back 200 OK
if it has some content to transfer back to the client or simply 204 No Content
if everything went as planned, but the server has nothing to show for it. There is also the option of returning 202 Accepted
if the server is planning to delete the resource later, but hasn’t had time to do it before the response was sent back.
For our little API, we will delete the user and return 204 No Content
.
# webapi.rb
# ...
# get '/users'
# get '/users/:first_name'
# head '/users'
# post '/users'
# put '/users/:first_name'
# patch '/users/:first_name'
delete '/users/:first_name' do |first_name|
users.delete(first_name.to_sym)
status 204
end
Restart your server after adding this route.
Give it a try with this curl
request.
curl -X DELETE -v http://localhost:4567/users/thibault
Request Sent:
DELETE /users/thibault HTTP/1.1
Host: localhost:4567
User-Agent: curl/7.43.0
Accept: */*
Response Received:
HTTP/1.1 204 No Content
X-Content-Type-Options: nosniff
Connection: close
Server: thin
We can then request the list of all users:
curl http://localhost:4567/users
Output
[
{"first_name":"Simon", "last_name":"Random", "age":26},
{"first_name":"John", "last_name":"Smith", "age":28}
]
Thibault is nowhere to be found, our DELETE
request did its job. We finally got rid of him!
The OPTIONS
method is a way for the client to ask the server what the requirements are for the identified resource. For example, OPTIONS
can be used to ask a server what HTTP methods can be applied to the identified resource or which source URL is allowed to communicate with that resource.
Cross-origin HTTP requests are initiated from one domain (for example, a.com), and sent to another domain (b.com). Stylesheets and images can be loaded from other domain names with cross-origin requests. However, browsers prevent such requests from happening from within scripts (except for GET
, HEAD
and POST
requests) and only if the server replies with the Access-Control-Allow-Origin
header that allows the sending domain.
For other methods, like PUT
or PATCH
, a preflight request will be sent with the OPTIONS
method in order to verify that the server allows this origin to perform this kind of action. We will setup CORS in the next module when we build a Rails 5 API.
It’s important to know about this HTTP method if you are building Single Page Applications with JavaScript frameworks like Angular.js or React.
Minimally, a server should respond with 200 OK
to an OPTIONS
request and include the Allow
header that contains the list of supported HTTP methods. Optimally, the server should send back the details of the possible operations in a documented style.
When requested with OPTIONS
, our resources should just send back the list of allowed HTTP methods in the Allow
header. In the second module, we will learn more about how to handle CORS and the family of headers starting with Access-Control-Allow-XXX
.
We need to add two routes in our Sinatra API: one for the users resource and one that will match any specific user resource.
# webapi.rb
# ...
# get '/users'
# get '/users/:first_name'
# head '/users'
# post '/users'
# put '/users/:first_name'
# patch '/users/:first_name'
# delete '/users/:first_name'
options '/users' do
response.headers['Allow'] = 'HEAD,GET,POST'
status 200
end
options '/users/:first_name' do
response.headers['Allow'] = 'GET,PUT,PATCH,DELETE'
status 200
end
Restart your server after adding these routes.
Let’s give it a try, as usual, with a quick curl
request.
curl -v -X OPTIONS http://localhost:4567/users
Request Sent:
OPTIONS /users HTTP/1.1
Host: localhost:4567
User-Agent: curl/7.43.0
Accept: */*
Response Received:
HTTP/1.1 200 OK
Allow: HEAD,GET,POST
[More Headers]
If we didn’t know how the server was built, this request would let us know that we can send three types of requests to the resource identified by /users
: HEAD
, GET
or POST
. By associating that with what we know about HTTP, we can already figure out that we can retrieve or create new users!
Let’s try with the route for a specific user now.
curl -i -X OPTIONS http://localhost:4567/users/thibault
Output
HTTP/1.1 200 OK
Allow: GET,PUT,PATCH,DELETE
[More Headers]
Alright, seems we’re done with OPTIONS
and everything is running smoothly.
We are getting to the HTTP methods that are very rarely used. Actually, we won’t be using any of these two methods since they are far from being useful when creating a web API. They are only meant to be used with HTTP proxies and won’t be covered in this book.
LINK
and UNLINK
are two new HTTP methods defined in an Internet-Draft. The idea behind those two methods is to allow the creation of relationships between entities. We will learn more about those two headers later in this book.
a ||= b
is the equivalent of a || a = b
and not a = a || b
, as some people think. Indeed, if a
already holds a value, why re-assign it?
We are done reviewing and integrating almost all the HTTP verbs into our Sinatra API; next, we’ll add error handling. Indeed, while building our endpoints, we forgot to consider that clients can make mistakes or just be plain deceptive. We need to protect ourselves against things like invalid data or missing users.
But first, we need to refactor our code! There is currently a lot of repeated code - and since we all love clean code, we urgently need to fix it.
There are two things we can improve quickly. First, we can extract the method type = accepted_media_type
that is at the beginning of some of our requests into a method that will load it when we need it and then just reuse it: @type ||= accepted_media_type
.
The second thing we can change is the code that sends back either JSON
or XML
to the client. The same piece of code is duplicated in multiple places, so we are going to extract it and put it inside a method. See below:
def send_data(data = {})
if type == 'json'
content_type 'application/json'
data[:json].call.to_json if data[:json]
elsif type == 'xml'
content_type 'application/xml'
Gyoku.xml(data[:xml].call) if data[:xml]
end
end
In this method, we expect to have either the :json
key, the :xml
key or none of them. The values for those keys should be lambdas that contain the data we want to send back. Why lambdas? So we don’t have to build multiple objects that wouldn’t be used.
Here is the complete code with the bit of refactoring I just mentioned. I also reorganized the methods to have all the endpoints related to /users
first, followed by the endpoints for /users/:first_name
.
# webapi.rb
require 'sinatra'
require 'json'
require 'gyoku'
users = {
thibault: { first_name: 'Thibault', last_name: 'Denizet', age: 25 },
simon: { first_name: 'Simon', last_name: 'Random', age: 26 },
john: { first_name: 'John', last_name: 'Smith', age: 28 }
}
helpers do
def json_or_default?(type)
['application/json', 'application/*', '*/*'].include?(type.to_s)
end
def xml?(type)
type.to_s == 'application/xml'
end
def accepted_media_type
return 'json' unless request.accept.any?
request.accept.each do |type|
return 'json' if json_or_default?(type)
return 'xml' if xml?(type)
end
halt 406, 'Not Acceptable'
end
def type
@type ||= accepted_media_type
end
def send_data(data = {})
if type == 'json'
content_type 'application/json'
data[:json].call.to_json if data[:json]
elsif type == 'xml'
content_type 'application/xml'
Gyoku.xml(data[:xml].call) if data[:xml]
end
end
end
get '/' do
'Master Ruby Web APIs - Chapter 2'
end
# /users
options '/users' do
response.headers['Allow'] = 'HEAD,GET,POST'
status 200
end
head '/users' do
send_data
end
get '/users' do
send_data(json: -> { users.map { |name, data| data.merge(id: name) } },
xml: -> { { users: users } })
end
post '/users' do
user = JSON.parse(request.body.read)
users[user['first_name'].downcase.to_sym] = user
url = "http://localhost:4567/users/#{user['first_name']}"
response.headers['Location'] = url
status 201
end
# /users/:first_name
options '/users/:first_name' do
response.headers['Allow'] = 'GET,PUT,PATCH,DELETE'
status 200
end
get '/users/:first_name' do |first_name|
send_data(json: -> { users[first_name.to_sym].merge(id: first_name) },
xml: -> { { first_name => users[first_name.to_sym] } })
end
put '/users/:first_name' do |first_name|
user = JSON.parse(request.body.read)
existing = users[first_name.to_sym]
users[first_name.to_sym] = user
status existing ? 204 : 201
end
patch '/users/:first_name' do |first_name|
user_client = JSON.parse(request.body.read)
user_server = users[first_name.to_sym]
user_client.each do |key, value|
user_server[key.to_sym] = value
end
send_data(json: -> { user_server.merge(id: first_name) },
xml: -> { { first_name => user_server } })
end
delete '/users/:first_name' do |first_name|
users.delete(first_name.to_sym)
status 204
end
Here is a list of curl
requests to ensure that everything is still working well. It would be even better had we written some automated tests…
curl -i http://localhost:4567/users
Output
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 273
X-Content-Type-Options: nosniff
Connection: keep-alive
Server: thin
[
{"first_name":"Thibault", "last_name":"Denizet", "age":25, "id":"thibault"},
{"first_name":"Simon", "last_name":"Random", "age":26, "id":"simon"},
{"first_name":"John", "last_name":"Smith", "age":28, "id":"john"}
]
curl -X POST -i http://localhost:4567/users \
-H "Content-Type: application/json" \
-d '{"first_name":"Samuel", "last_name":"Da Costa", "age":19}'
Output
HTTP/1.1 201 Created
Content-Type: text/html;charset=utf-8
Location: http://localhost:4567/users/Samuel
Content-Length: 0
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Connection: keep-alive
Server: thin
curl -X PUT -i http://localhost:4567/users/jane \
-H "Content-Type: application/json" \
-d '{"first_name":"Jane", "last_name":"Smith", "age":25}'
Output
HTTP/1.1 201 Created
Content-Type: text/html;charset=utf-8
Content-Length: 0
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Connection: keep-alive
Server: thin
curl -i http://localhost:4567/users/jane
Output
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 62
X-Content-Type-Options: nosniff
Connection: keep-alive
Server: thin
{"first_name":"Jane", "last_name":"Smith", "age":25, "id":"jane"}
curl -X PATCH -i http://localhost:4567/users/thibault \
-H "Content-Type: application/json" \
-d '{"age":26}'
Output
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 72
X-Content-Type-Options: nosniff
Connection: keep-alive
Server: thin
{"first_name":"Thibault", "last_name":"Denizet", "age":26, "id":"thibault"}
curl -X DELETE -i http://localhost:4567/users/thibault
Output
HTTP/1.1 204 No Content
X-Content-Type-Options: nosniff
Connection: close
Server: thin
curl -i http://localhost:4567/users
Output
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 263
X-Content-Type-Options: nosniff
Connection: keep-alive
Server: thin
[
{"first_name":"Simon", "last_name":"Random", "age":26, "id":"simon"},
{"first_name":"John", "last_name":"Smith", "age":28, "id":"john"},
{"first_name":"Samuel", "last_name":"Da Costa", "age":19, "id":"samuel"},
{"first_name":"Jane", "last_name":"Smith", "age":25, "id":"jane"}
]
curl -i -X OPTIONS http://localhost:4567/users
Output
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Allow: HEAD,GET,POST
Content-Length: 0
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Connection: keep-alive
Server: thin
Everything looks good. Now our API actually looks like something!
In this chapter, we’ve learned what HTTP methods are and how they are supposed to be used. We also used curl
extensively in order to test all that. Now it’s time to handle some errors!