3️⃣POST Requests

Fall 2023 | Vin Bui

Creating a POST Request

Inside of our NetworkManager class, we write functions to be called by our application. These functions will use Alamofire to send HTTP requests.

Assume there is a struct called Member that represents an AppDev member.

struct Member: Codable {
    let name: String
    let subteam: String
    let position: String
}

We can write the following code to add a member to the AppDev roster store in the backend.

// 1. Create the function
func addToRoster(member: Member, completion: @escaping (Member) -> Void) {
    // 2. Specify the endpoint
    let endpoint = "<Enter URL String Here>"

    // 3. Define the request body
    let parameters: Parameters = [
	"name": member.name,
	"subteam": member.subteam,
	"position": member.position
    ]
    
    // 4. Create a decoder
    let decoder = JSONDecoder()
    // decoder.dateDecodingStrategy = .iso8601 // Only if needed
    // decoder.keyDecodingStrategy = .convertFromSnakeCase // Only if needed
    
    // 5. Create the request
    AF.request(endpoint, method: .post, parameters: parameters, encoding: JSONEncoding.default)
        .validate()
        .responseDecodable(of: Member.self, decoder: decoder) { response in
	    // 5. Handle the response
            switch response.result {
            case .success(let member):
                print("Successfully added member \(member.name)")
                completion(member)
            case .failure(let error):
                print("Error in NetworkManager.addToRoster: \(error)")
            }
        }
}

Let’s break down this code.

  1. Create a function called fetchRoster that takes in a callback (completion handler) as an argument. This callback takes in a Member object. Note that this depends on what this POST request returns from the backend. However, we can expect the backend to return the member that we just added back to us.

  2. Specify the endpoint which is the URL that we will call to fetch the data. We can test this with Postman.

  3. Define the request body. This POST request takes in three fields inside of the request body. Using the member parameter storing the Member object that we passed in, we give values to these fields. Note that we do not necessarily need to have this member parameter. If we want, we can have three separate arguments passed into the function instead of one.

  4. Create a decoder to decode the data. If our object contains a Date property, then we need to specify the date decoding strategy. If the JSON contains fields with snake_case, then we also need to specify the key decoding strategy.

  5. Create the request using Alamofire.

    • Pass in the endpoint and specify the method (.get for GET, .post for POST, etc).

    • Encode the request with JSON to be sent over the internet (POST).

    • Validate the request to ensure that the status code is 2xx and if the content type matches.

    • Decode the response to Member using the decoder we created in Step 4.

  6. Perform a switch statement on the response’s result.

    • If successful, pass to the callback the decoded response. A print statement is optional but recommended.

    • If failed, print an error statement about the error.

Note that we do not have to create the decoder inside of the function call. We can create a JSONDecoder object and store it as a property in our NetworkManager class to be used for all functions. The same can be applied to the endpoint variable containing a String value of our endpoint.

Let’s point out the differences between this and a GET request.

  1. The function header usually contains a parameter storing some value that we want to send to the backend. In this case, we had a parameter called member storing a Member object.

  2. We defined a dictionary whose type is Parameters. Remember, these are key-value pairs.

  3. Our method is .post instead of .get. We also encode it using JSONEncoding.default.

  4. We pass in parameters to the AF.request function. In the GET request version, we did not do this.

Calling the Network Request

NetworkManager.shared.addToRoster(member: someMemberVariable) { fetchedMember in
    // Do something with the data
    
    DispatchQueue.main.async {
        // Perform UI updates
    }
}

Or if we plan on using self somewhere,

NetworkManager.shared.addToRoster(member: someMemberVariable) { [weak self] fetchedMember in
    // Unwrap a strong reference
    guard let self = self else { return }

    // Do something with the data
    
    DispatchQueue.main.async {
        // Perform UI updates
    }
}

This is exactly the same as it was with a GET request except that we have to pass in an argument to the function we created.

Success or Failure?

In the code samples above, our callback took in a Member object. Sometimes the backend does not return this information, or we may not even need this information. Instead, it may be more useful for the callback to hold a more useful variable type: a Bool.

If we were to refactor our code, it would look something like this:

// 1. Create the function
func addToRoster(member: Member, completion: @escaping (Bool) -> Void) {
    // 2. Specify the endpoint
    let endpoint = "<Enter URL String Here>"

    // 3. Define the request body
    let parameters: Parameters = [
	"name": member.name,
	"subteam": member.subteam,
	"position": member.position
    ]
    
    // 4. Create a decoder
    let decoder = JSONDecoder()
    // decoder.dateDecodingStrategy = .iso8601 // Only if needed
    // decoder.keyDecodingStrategy = .convertFromSnakeCase // Only if needed
    
    // 5. Create the request
    AF.request(endpoint, method: .post, parameters: parameters, encoding: JSONEncoding.default)
        .validate()
        .responseDecodable(of: Member.self, decoder: decoder ) { response in
	    // 5. Handle the response
            switch response.result {
            case .success(let member):
                print("Successfully added member \(member.name)")
                completion(true)
            case .failure(let error):
                print("Error in NetworkManager.addToRoster: \(error)")
		completion(false)
	    }
        }
}
NetworkManager.shared.addToRoster(member: someMemberVariable) { success in
    if success {
	// Do something if the call succeeded
    } else {
	// Do something if the call failed
    }
}

The main difference here is that the callback takes in a Bool instead of the Member object. If the response succeeds, we use completion(true) to pass in true to the callback. If the response fails, we use completion(false).

When we call this function, we use success to access the value passed to the callback. We can then do whatever we need depending on the status of the network request.

Last updated