Perfect: Server-side Swift - Using form data
Hello and welcome back,
This is the third 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
Ok, now that everyone's caught up, lets continue our journey - Using form data we're sending our Swift app.
In the last episode we've added a new route (POST on "/") and we've tested it using a print command. Once we're aiming to build an API, posting data on the home route ("/") isn't that useful, instead, lets remove the previous route and add a route and handler to register users in our app.
For the time being we will not store these users, but that will come in future episodes - for now we'll grab the data sent to us and return it in a nice format back to the sender.
Cleaning up our code
First, lets remove the existing route and handler that we have added earlier. Remove the line from AppRoutes.swift reading
routes.add(method: .post, uri: "/", handler: homePostHandler)
and also the handler function, homePostHandler, from the homeHandler.swiftfile.
func homePostHandler(request: HTTPRequest, _ response: HTTPResponse) {
print("Uh oh, someone just called / using POST method!!!")
response.completed()
}
New route and handler
Next, lets add a new route, using the POST method, responding to the "/users" URI and being handled by the addUserHandler handler function. Don't worry, we'll get some errors but those are just because we haven't created the handler yet.
Just like before, add a new file to the project, in the same location as main.swiftand the other files, and name this one usersHandler.swift. Don't forget to check your app in the Target Membership section on the right panel (Xcode). In this file we'll add all the handlers that will operate with users.
The routes file should look like this
import PerfectHTTP
func setupAppRoutes() -> Routes {
var routes = Routes()
routes.add(method: .get, uri: "/", handler: homeHandler)
//MARK: Users
routes.add(method: .post, uri: "/users", handler: addUserHandler)
return routes
}
and the new handler file show look like this
import PerfectHTTP
func addUserHandler(request: HTTPRequest, _ response: HTTPResponse) {
response.completed()
}
Before proceeding any further lets take a step back and see why did we chose a POST method and why did we chose "/users" and not "/users/add" or "/users/create".
We are trying to build an API, but we're also trying to adhere to some standard, so the next developer that comes into the project later on can easily understand what we have done. The standard I'm referring to is called REST, and it's not really a standard but more of a set of guidelines - You know, just like the Brethren Code from Pirates of the Caribbean...
Barbossa: First, your return to shore was not part of our negotiations nor our agreement so I must do nothing. And secondly, you must be a pirate for the pirate's code to apply and you're not. And thirdly, the code is more what you'd call "guidelines" than actual rules.
I would suggest a read of the 10 Best practices for better RESTful API article, which makes it clear how we should architect our routes and verbs.
TL;DR;
- Use nouns but no verbs - For an easy understanding use this structure for every resource: GET = Read, POST = Create, PUT = Update, DELETE = well... delete.
- GET method and query parameters should not alter the state - Use PUT, POST and DELETE methods instead of the GET method to alter the state.
- Use plural nouns - /users instead of /user, /settings instead of /setting
- Use sub-resources for relations - GET /cars/711/drivers/ Returns a list of drivers for car 711 while also GET /cars/711/drivers/4 Returns driver #4 for car 711
- Use HTTP headers for serialisation formats - Content-Type defines the request format. and Accept defines a list of acceptable response formats.
- Use HATEOAS - Hypermedia as the Engine of Application State is a principle that hypertext links should be used to create a better navigation through the API.
- Provide filtering, sorting, field selection and paging for collections
- Version your API - We'll get to that in the following episodes actually
- Handle Errors with HTTP status codes
- Allow overriding HTTP method
But enough of a side-note, lets continue with the example. Usually you would take another step back here and think what a user entity would need when it comes to parameters. Normally these parameters should contain a first name, a last name, potentially an email address and maybe a socialID of some sort like Facebook or Twitter or even Google - it really depends on what you are trying to accomplish and what you request from the user registering with your API.
For now we'll stick with firstName (required), lastName (required) and emailAddress (optional). In a future episode we'll come back and add an avatar image as well.
Ok, so now that we know what we're expecting from the call to provide to us, we can continue our coding.
Reading sent parameters
The people @Perfect, in their infinite wisdom, gave us a few ways to access the parameters sent to our route. By using the request parameter of our addUserHandler we can call a few methods to find out what form parameters we have received.
We have request.params() which will return an array containing all parameters sent, query and POST.
We could also use request.postParams() which will also return an array of strings, but as the name suggests, only the parameters received within a POST call.
We also have request.params(named: String) which we can use to pull multiple values of the same parameter, like multiple choice checkboxes on a page / form.
And last, but not least, we have request.param(name: String) which permits us to pull in a parameter by key - and when I say key I am referring to firstNamefor example. This method also has an alternative, request.param(name: String, defaultValue: String?) which will allow you to optionally specify a default value, in case the parameter is not present in the call.
I usually like code to be clean and simple to read - you should always think about this quote when writing a new piece of code
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live. ― John Woods
With that in mind we will use the last method listed, we're going to pull in each parameter, check if the required ones were provided, if not - return an error, if yes - format a nice message and return it to the user calling.
Fetching the data
So, using the request.param(name: String, defaultValue: String?) method we start pulling in our parameters...
let firstName = request.param(name: "firstName", defaultValue: "") let lastName = request.param(name: "lastName", defaultValue: "") let emailAddress = request.param(name: "emailAddress", defaultValue: "")!
I've decided to use constants (let) and not variables (var) because these values should not be changed. There are times when you might need to alter one or more received values, but in our case we're not going to go into that.
Next we should check if our required parameters, firstName and lastName are there. I have given them an empty string as a default value, which will help with the next checks.
To test the empty values, I have chosen the guard statement instead of the plain if - Read more on guard here - see section Using guard case.
Let's add our first guard case, should look like this
guard !(firstName?.isEmpty)! else {
response.appendBody(string: "First name is required!")
response.completed()
return
}
This will test if the optional value inside firstName is empty, and if it is it will continue executing the else statement. This is a simplified error we can return to our client letting it know that the API hasn't received a mandatory parameter.
Following the same pattern, we write the second test for our second required parameter, lastName.
guard !(lastName?.isEmpty)! else {
response.appendBody(string: "Last name is required!")
response.completed()
return
}
We are not going to bother with emailAddress since it is an optional parameter, the call might or might not contain it - of course we could go in and validate it if the call has that parameter, but not in this example.
Since our guard statements we'll throw an error we can safely compose the success message after the second guard and return it to our caller. The last piece of code should be looking like this
var returnMessage: String = "Oh hay! I see you have sent me a user called \(firstName), \(lastName)."
if !(emailAddress.isEmpty) {
returnMessage += " I also see \(emailAddress) as his/her email address"
}
response.appendBody(string: returnMessage)
Because we're sending a nicely formatted message back, I have added an ifstatement which checks if the emailAddress parameter is not empty so it adds a customised message at the end.
The full file code now should be similar to this
import PerfectHTTP
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
}
var returnMessage: String = "Oh hay! I see you have sent me a user called \(firstName!), \(lastName!)."
if !(emailAddress.isEmpty) {
returnMessage += " I also see \(emailAddress) as his/her email address"
}
response.appendBody(string: returnMessage)
response.completed()
}
which means we're pretty much done with the coding - all that it remains is to test it.
Testing our parameters
As per the previous episode, you can use curl or any other REST client you are comfortable with to run the tests, I am going to do it using curl.
Successful test
curl -X POST -F 'firstName=Robert' -F 'lastName=Bojor' http://localhost:8181/users Output: Oh hay! I see you have sent me a user called Robert, Bojor.
Full parameters test
curl -X POST -F 'firstName=Robert' -F 'lastName=Bojor' -F 'emailAddress=robert@email.com' http://localhost:8181/users Output: Oh hay! I see you have sent me a user called Robert, Bojor. I also see robert@email.com as his/her email address
Failed test 1 - No firstName
curl -X POST -F 'lastName=Bojor' http://localhost:8181/users Output: First name is required!
Failed test 2 - No lastName
curl -X POST -F 'firstName=Robert' http://localhost:8181/users Output: Last name is required!
Success!
You now can go forward and add more parameters to your calls, test if they exist or not, return custom errors to the caller or return a success message.
As a side note, you should always test all your cases before proclaiming that the code works flawlessly.
Until the next time...
Have fun and keep coding,
Robert B.