Perfect: Server-side Swift - Responses explained
This is the fifth part in our journey towards building a functional API using Swift 3 and Perfect. If you just tuned in, you can read the previous guides here:
Part 0 (Pilot) - Setting up a DigitalOcean instance to run our code live
Part 1 - Perfect: Server-side Swift - Refactoring routes and handlers
Part 2 - Perfect: Server-side Swift - Handling requests
Part 3 - Perfect: Server-side Swift - Using form data
Part 4 - Perfect: Server-side Swift - Handling GET parameters
Ok, now that everyone's caught up, lets continue our journey - we're going to take a look at the responses your app can return to the caller.
Important
When a request has completed its course and the business logic has finished processing/interacting with the data, it is required that the HTTPResponse's .completed() function be called. Calling it will ensure that all pending data is delivered to the client, and the established TCP connection will be either closed, or in the case of HTTP keep-alive, a new request will be read and processed.
The available methods that the HTTPResponse object exposes are useful when it comes to return a response to the caller. Remember, in one of our previous guide "Using form data" we have already used one of the methods - .appendBody(string: String) - which was just adding a normal string to the body of the response, and when the .completed() method was called the app returned it back to the caller.
Lets have a look at the most used methods exposed by HTTPResponse...
addHeader(named: HTTPResponseHeader.Name, value: String)
Mostly used for adding custom headers that will match the content we are returning to the caller. Because what we are building is supposed to be an API, your responses will consist in one type and one type only application/jsona.k.a JSON - but this is just an example of a header type you can return, the list is really long.
response.addHeader(.contentType, value: "application/json")
For example, another header type you can set is .contentEncoding which will tell your caller the type of encoding the data you are returning has.
.setBody()
This method has a few options when called...
- You can set the body of the response using a simple string .setBody(string: String),
- You can set it with an object of type [String: Any] and using .setBody(json: [String: Any]) will have the method encode that object into a JSON string,
- You can also set raw body bytes if you want, using .setBody(bytes: [UInt8])
The next one works hand in hand with setBody...
.appendBody()
Which will append more data to an already set body content. You can actually don't need to call setBody before using appendBody.
.appendBody has two options when is called...
- append a simple string using .appendBody(string: String), or
- append raw bytes, just like above, by calling .appendBody(bytes: [UInt8])
Another useful and most used method when dealing with APIs is...
.status
The response status plays an important role when you successfully return data to your caller and when you have to tell your caller that the operation ended up in error.
If we take a look at the underlying code for .status we will notice that its type is HTTPResponseStatus and having a look even further at this type we will see a plethora of response codes.
Don't panic, when you work with response codes every day you will get to know almost all of them by heart. Lets set a 500 Internal Server Error status code back to our caller
response.status = HTTPResponseStatus.internalServerError
If we take a look at one curl request that has this status set we will see
< HTTP/1.1 500 Internal Server Error < Content-Type: application/json < Connection: Keep-Alive < Content-Length: 0
There are many other available methods for the HTTPResponse body, and I encourage you to experiment, and if you want additional details, have a look at Perfect's official documentation for HTTPResponse.
Let's put some of these methods to the test in our app. Remember that we have a route that was expecting firstName, lastName and emailAddressparameters?
When we first wrote the logic for that route we were returning a simple string letting our caller know that we received the parameters - let's change it to return a JSON string.
Good practice tip
When creating or refactoring any piece of code it is always recommended to list all the tasks you need to accomplish before setting out to write the code - you can use commented lines within that function and under each comment you can type the code for that task
So, following the tip above, our refactored handler should initially look like this
func addUserHandler(request: HTTPRequest, _ response: HTTPResponse) {
let firstName = request.param(name: "firstName", defaultValue: "")
let lastName = request.param(name: "lastName", defaultValue: "")
let emailAddress = request.param(name: "emailAddress", defaultValue: "")
guard !(firstName?.isEmpty)! else {
response.appendBody(string: "First name is required!")
response.completed()
return
}
guard !(lastName?.isEmpty)! else {
response.appendBody(string: "Last name is required!")
response.completed()
return
}
// Build the object that will be returned
// Unwrap and add firstName to the returned object
// Unwrap and add lastName to the returned object
// Add emailAddress to the object if set
// Set response content type as application/json
// Set response status code as 200 OK
// Set the response body with the above object
// Complete the response
response.completed()
}
I have added the unwrap tasks there for safety reasons, it's better to be safe from the beginning than to realise later there's a fatal error somewhere.
Also notice that we have removed the forceful unwrap from the request.paramline for emailAddress!
Challenge
Try to code the tasks yourself before looking at the finished code below, see if you get it right...
func addUserHandler(request: HTTPRequest, _ response: HTTPResponse) {
let firstName = request.param(name: "firstName", defaultValue: "")
let lastName = request.param(name: "lastName", defaultValue: "")
let emailAddress = request.param(name: "emailAddress", defaultValue: "")
guard !(firstName?.isEmpty)! else {
response.appendBody(string: "First name is required!")
response.completed()
return
}
guard !(lastName?.isEmpty)! else {
response.appendBody(string: "Last name is required!")
response.completed()
return
}
// Build the object that will be returned
var userObject: [String: Any] = [:]
// Unwrap and add firstName to the returned object
if let userFirstName = firstName {
userObject["firstName"] = userFirstName
}
// Unwrap and add lastName to the returned object
if let userLastName = lastName {
userObject["lastName"] = userLastName
}
// Add emailAddress to the object if set
if let userEmailAddress = emailAddress {
userObject["emailAddress"] = userEmailAddress
}
// Set response content type as application/json
response.setHeader(.contentType, value: "application/json")
// Set response status code as 200 OK
response.status = HTTPResponseStatus.ok
// Set the response body with the above object
do {
try response.setBody(json: userObject)
} catch {
response.status = HTTPResponseStatus.internalServerError
response.appendBody(string: "{\"error\":\"Could not set body!\"}")
}
// Complete the response
response.completed()
}
Ok, so lets see what's going on here, task by task
- Build the object that will be returned - We have initialised a variable userObject of type [String: Any] to an empty dictionary. Why that type? If you check the type of setBody you will notice that it accepts exactly this type.
- Unwrap and add firstName to the returned object - To be careful and not add an invalid type to our object we have used the if let syntax here to unwrap the optional variable.
- Unwrap and add lastName to the returned object - Same logic applies for lastName as well.
- Add emailAddress to the object if set - Because we removed the force unwrap from the line where this variable was set, we were "forced" to do it here using the same if let logic.
- Set response content type as application/json - Pretty straight forward I would say... We used the .setHeader method and application/json value to tell our caller that we're returning a JSON string and not a normal text.
- Set response status code as 200 OK - You might wonder why we haven't set this one to 200 before now, well, that is because 200 is the default return status code, but it's still best practice if you set it yourself - other developer reading your code will see it straight away and understand it.
- Set the response body with the above object - This is where it gets a bit tricky. The method .setBody is marked with throws which means it could throw an error when trying to override/set the response body. In this case we are required to use a do {} catch {} block, and inside the do block we tell Swift to try and set the body. If all is successful the execution will continue, but if an error is thrown, the catch block will be executed instead. In the catch block I have changed the status code for the response to 500 (to signal an error) and also appended a manually written JSON string as an error. The manual JSON string is not quite a good practice but we haven't touched the JSON subject yet - we will.
Now, all that it remains is to test this code and see if it works.
curl -v -X POST -F 'firstName=Robert' -F 'lastName=Bojor' -F 'emailAddress=robert@email.com' http://localhost:8181/users
I have added the verbose flag so we can see the headers of the returned data...
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8181 (#0)
> POST /users HTTP/1.1
> Host: localhost:8181
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Length: 369
> Expect: 100-continue
> Content-Type: multipart/form-data; boundary=------------------------5b955ecd94f0041f
>
* Done waiting for 100-continue
< HTTP/1.1 200 OK
< Content-Type: application/json
< Connection: Keep-Alive
< Content-Length: 75
<
* Connection #0 to host localhost left intact
{"lastName":"Bojor","emailAddress":"robert@email.com","firstName":"Robert"}
If we check the output, the lines starting with <, we can see a response code of 200, the Content-Type set to application/json and the last line contains our JSON string.
Success!
Now you customise your app's response object as much as you want/need and you can return any type of data you might need.
On our next guide we will take a look at JSON, how to create it, how to operate with it, so you won't have to manually build an error string.
Have fun and keep coding,
Robert B.