In this three-part series, we have been covering all the basics of what it takes to become a backend developer. This is a tutorial for mobile app developers.
So far we have already learned about REST APIs and how they use the HTTP protocol for client server communication. In the last lesson, we learned how to implement our API on the server, once using Node.js and again using Server Side Dart. I chose Node.js because it is popular and Server Side Dart because it allows Flutter developers to use Dart everywhere. However, it really doesn't matter what platform or language you chose for the backend server. Because we are using a REST API, we are able to make requests to it with Android, iOS, Futter, or any other frontend platform.
In this tutorial, we will look at how to make a frontend client app that connects to the server we made in part two, I’ll give you three different examples: one for Android, one for iOS, and one for Flutter. Since you are already a mobile app developer, I won't go into much detail about how to create the app or build the layout. Instead I'll give you the code for making the HTTP requests.
Just scroll down to the platform you are developing with. If you are a developer for a platform other than Android, iOS, or Flutter, you can look up the code for making HTTP requests on your platform and just port one of the examples below.
I'm assuming that you already have experience with Android, iOS, or Flutter, and that you have the development environment set up. I wrote and tested the client apps using the following software versions:
Create a layout similar to the image below (see source code):
In the manifest add the INTERNET
permission (see source code):
<uses-permission android:name="android.permission.INTERNET" />
And allow clear text:
1<application 2 android:usesCleartextTraffic="true" 3 ... 4 >
Note: You should use a secure HTTPS server in production, but in this tutorial we are using "clear text" HTTP. Doing this allowed me to simplify the server tutorial in part two by not having to register a certificate with a certificate authority.
Replace MainActivity.kt
with the following code (Java version here):
1package com.example.backendclient 2 3 import android.support.v7.app.AppCompatActivity 4 import android.os.Bundle 5 import android.os.AsyncTask 6 import android.util.Log 7 import android.view.View 8 import java.io.* 9 import java.net.HttpURLConnection 10 import java.net.URL 11 12 private const val HOST = "http://10.0.2.2:3000" 13 private const val TAG = "TAG" 14 15 class MainActivity : AppCompatActivity() { 16 17 override fun onCreate(savedInstanceState: Bundle?) { 18 super.onCreate(savedInstanceState) 19 setContentView(R.layout.activity_main) 20 } 21 22 fun makeGetAllRequest(view: View) { 23 HttpGetRequest().execute(HOST) 24 } 25 26 fun makeGetOneRequest(view: View) { 27 val idToGet = 0 28 val url = "$HOST/$idToGet" 29 HttpGetRequest().execute(url) 30 } 31 32 fun makePostRequest(view: View) { 33 val json = "{\"fruit\": \"pear\", \"color\": \"green\"}" 34 HttpPostRequest().execute(HOST, json) 35 } 36 37 fun makePutRequest(view: View) { 38 val idToReplace = 0 39 val url = "$HOST/$idToReplace" 40 val json = "{\"fruit\": \"watermellon\", \"color\": \"red and green\"}" 41 HttpPutRequest().execute(url, json) 42 } 43 44 fun makePatchRequest(view: View) { 45 val idToUpdate = 0 46 val url = "$HOST/$idToUpdate" 47 val json = "{\"color\": \"green\"}" 48 HttpPatchRequest().execute(url, json) 49 } 50 51 fun makeDeleteRequest(view: View) { 52 val idToDelete = 0 53 val url = "$HOST/$idToDelete" 54 HttpDeleteRequest().execute(url) 55 } 56 57 // GET 58 class HttpGetRequest : AsyncTask<String, Void, Void>() { 59 override fun doInBackground(vararg params: String): Void? { 60 val urlString = params[0] 61 62 val myUrl = URL(urlString) 63 val connection = myUrl.openConnection() as HttpURLConnection 64 connection.requestMethod = "GET" 65 val result = getStringFromInputStream(connection.inputStream) 66 val statusCode = connection.responseCode 67 connection.disconnect() 68 69 Log.i(TAG, "GET result: $statusCode $result") 70 return null 71 } 72 } 73 74 // POST 75 class HttpPostRequest : AsyncTask<String, Void, Void>() { 76 override fun doInBackground(vararg params: String): Void? { 77 val urlString = params[0] 78 val json = params[1] 79 80 val myUrl = URL(urlString) 81 val connection = myUrl.openConnection() as HttpURLConnection 82 connection.requestMethod = "POST" 83 connection.doOutput = true 84 connection.setRequestProperty("Content-Type", "application/json") 85 86 writeStringToOutputStream(json, connection.outputStream) 87 val result = getStringFromInputStream(connection.inputStream) 88 val statusCode = connection.responseCode 89 connection.disconnect() 90 91 Log.i(TAG, "POST result: $statusCode $result") 92 return null 93 } 94 } 95 96 // PUT 97 class HttpPutRequest : AsyncTask<String, Void, Void>() { 98 override fun doInBackground(vararg params: String): Void? { 99 val urlString = params[0] 100 val json = params[1] 101 102 val myUrl = URL(urlString) 103 val connection = myUrl.openConnection() as HttpURLConnection 104 connection.requestMethod = "PUT" 105 connection.doOutput = true 106 connection.setRequestProperty("Content-Type", "application/json") 107 108 writeStringToOutputStream(json, connection.outputStream) 109 val result = getStringFromInputStream(connection.inputStream) 110 val statusCode = connection.responseCode 111 connection.disconnect() 112 113 Log.i(TAG, "PUT result: $statusCode $result") 114 return null 115 } 116 } 117 118 // PATCH 119 class HttpPatchRequest : AsyncTask<String, Void, Void>() { 120 override fun doInBackground(vararg params: String): Void? { 121 val urlString = params[0] 122 val json = params[1] 123 124 val myUrl = URL(urlString) 125 val connection = myUrl.openConnection() as HttpURLConnection 126 connection.requestMethod = "PATCH" 127 connection.doOutput = true 128 connection.setRequestProperty("Content-Type", "application/json") 129 130 writeStringToOutputStream(json, connection.outputStream) 131 val result = getStringFromInputStream(connection.inputStream) 132 val statusCode = connection.responseCode 133 connection.disconnect() 134 135 Log.i(TAG, "PATCH result: $statusCode $result") 136 return null 137 } 138 } 139 140 // DELETE 141 class HttpDeleteRequest : AsyncTask<String, Void, Void>() { 142 override fun doInBackground(vararg params: String): Void? { 143 val urlString = params[0] 144 145 val myUrl = URL(urlString) 146 val connection = myUrl.openConnection() as HttpURLConnection 147 connection.requestMethod = "DELETE" 148 149 val result = getStringFromInputStream(connection.inputStream) 150 val statusCode = connection.responseCode 151 connection.disconnect() 152 153 Log.i(TAG, "DELETE result: $statusCode $result") 154 return null 155 } 156 } 157 } 158 159 private fun writeStringToOutputStream(json: String, outputStream: OutputStream) { 160 val bytes = json.toByteArray(charset("UTF-8")) // API 19: StandardCharsets.UTF_8 161 outputStream.write(bytes) 162 outputStream.close() 163 } 164 165 private fun getStringFromInputStream(stream: InputStream): String { 166 val text = stream.bufferedReader().use { it.readText() } 167 stream.close() 168 return text 169 }
As you can see, Android uses HttpURLConnection to make HTTP requests. After opening the connection you use setRequestMethod()
to choose the HTTP verb that you want (GET
, POST
, etc.).
You send the request by writing data to an output stream. After that you get the response by reading from an input stream. This should all be done in an AsyncTask to avoid blocking the UI thread.
I used a raw string for the JSON in the code above. The GSON library is one option for converting JSON strings to Java objects. Check out this tutorial for some instruction on that.
With the server that you made in part two running, test the app in the Android emulator. In the Android Studio Logcat, note the statements that get printed after server responses:
1GET result: 200 [{"fruit":"apple","color":"red"},{"fruit":"banana","color":"yellow"}] 2 GET result: 200 {"fruit":"apple","color":"red"} 3 POST result: 200 Item added with id 2 4 PUT result: 200 Item replaced at id 0 5 PATCH result: 200 Item updated at id 0 6 DELETE result: 200 Item deleted at id 0
Create a layout similar to the image below:
In the Info.plist file, add the following key:
1<key>NSAppTransportSecurity</key>
2 <dict>
3 <key>NSAllowsArbitraryLoads</key>
4 <true/>
5 </dict>
Note: You should use a secure HTTPS server in production, but in this tutorial we are using unencrypted text with an HTTP server. Adding the key above bypasses iOS's requirement for encrypted text over a network call. Doing this allowed me to simplify the server tutorial in part two by not having to register a certificate with a certificate authority.
Replace ViewController.swift
with the following code:
1import UIKit
2 class ViewController: UIViewController {
3
4 let host = "http://localhost:3000"
5
6 // make GET (all) request
7 @IBAction func makeGetAllRequestTapped(_ sender: UIButton) {
8
9 guard let url = URL(string: host) else {return}
10
11 // background task to make request with URLSession
12 let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
13 guard let statusCode = (response as? HTTPURLResponse)?.statusCode
14 else {return}
15 guard let body = data
16 else {return}
17 guard let jsonString = String(data: body, encoding: String.Encoding.utf8)
18 else {return}
19
20 print("GET result: \(statusCode) \(jsonString)")
21 }
22
23 // start the task
24 task.resume()
25 }
26
27 // make GET (one) request
28 @IBAction func makeGetOneRequestTapped(_ sender: UIButton) {
29
30 let idToGet = 0;
31 let urlString = "\(host)/\(idToGet)"
32 guard let url = URL(string: urlString) else {return}
33
34 let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
35 guard let statusCode = (response as? HTTPURLResponse)?.statusCode
36 else {return}
37 guard let body = data
38 else {return}
39 guard let jsonString = String(data: body, encoding: String.Encoding.utf8)
40 else {return}
41
42 print("GET result: \(statusCode) \(jsonString)")
43 }
44 task.resume()
45 }
46
47 // make POST request
48 @IBAction func makePostRequestTapped(_ sender: UIButton) {
49
50 let dictionary = ["fruit" : "pear", "color" : "green"]
51
52 // prepare request
53 guard let url = URL(string: host) else {return}
54 var request = URLRequest(url: url)
55 request.httpMethod = "POST"
56 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
57 guard let json = try? JSONSerialization.data(
58 withJSONObject: dictionary, options: [])
59 else {return}
60 request.httpBody = json
61
62 let task = URLSession.shared.dataTask(with: request) {
63 (data, response, error) in
64 guard let statusCode = (response as? HTTPURLResponse)?.statusCode
65 else {return}
66 guard let body = data
67 else {return}
68 guard let responseString = String(data: body, encoding: .utf8)
69 else {return}
70 print("POST result: \(statusCode) \(responseString)")
71
72 // If your API returns JSON you could do the following:
73 // guard let jsonString = try? JSONSerialization.jsonObject(
74 // with: body, options: []) else {return}
75 }
76 task.resume()
77 }
78
79 // make PUT request
80 @IBAction func makePutRequestTapped(_ sender: UIButton) {
81
82 let dictionary = ["fruit" : "watermellon", "color" : "red and green"]
83
84 let idToPut = 0;
85 let urlString = "\(host)/\(idToPut)"
86
87 // prepare request
88 guard let url = URL(string: urlString) else {return}
89 var request = URLRequest(url: url)
90 request.httpMethod = "PUT"
91 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
92 guard let json = try? JSONSerialization.data(
93 withJSONObject: dictionary, options: [])
94 else {return}
95 request.httpBody = json
96
97 let task = URLSession.shared.dataTask(with: request) {
98 (data, response, error) in
99 guard let statusCode = (response as? HTTPURLResponse)?.statusCode
100 else {return}
101 guard let body = data
102 else {return}
103 guard let responseString = String(data: body, encoding: .utf8)
104 else {return}
105 print("PUT result: \(statusCode) \(responseString)")
106 }
107 task.resume()
108 }
109
110 // make PATCH request
111 @IBAction func makePatchRequestTapped(_ sender: UIButton) {
112
113 let dictionary = ["color" : "green"]
114
115 let idToPatch = 0;
116 let urlString = "\(host)/\(idToPatch)"
117
118 // prepare request
119 guard let url = URL(string: urlString) else {return}
120 var request = URLRequest(url: url)
121 request.httpMethod = "PATCH"
122 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
123 guard let json = try? JSONSerialization.data(
124 withJSONObject: dictionary, options: [])
125 else {return}
126 request.httpBody = json
127
128 let task = URLSession.shared.dataTask(with: request) {
129 (data, response, error) in
130 guard let statusCode = (response as? HTTPURLResponse)?.statusCode
131 else {return}
132 guard let body = data
133 else {return}
134 guard let responseString = String(data: body, encoding: .utf8)
135 else {return}
136 print("PATCH result: \(statusCode) \(responseString)")
137 }
138 task.resume()
139 }
140
141 // make DELETE request
142 @IBAction func makeDeleteRequestTapped(_ sender: UIButton) {
143
144 let idToDelete = 0;
145 let urlString = "\(host)/\(idToDelete)"
146
147 // prepare request
148 guard let url = URL(string: urlString) else {return}
149 var request = URLRequest(url: url)
150 request.httpMethod = "DELETE"
151
152 let task = URLSession.shared.dataTask(with: request) {
153 (data, response, error) in
154 guard let statusCode = (response as? HTTPURLResponse)?.statusCode
155 else {return}
156 guard let body = data
157 else {return}
158 guard let responseString = String(data: body, encoding: .utf8)
159 else {return}
160 print("DELETE result: \(statusCode) \(responseString)")
161 }
162 task.resume()
163 }
164 }
As you can see, iOS uses URLSession to make HTTP requests as URLRequest. It will return a URLResponse from the server. We used JSONSerialization to convert the JSON strings to and from Swift Dictionary objects.
With the server that you made in part two running, test the app in the iOS simulator. Note the log statements that get printed in Xcode after server responses:
1GET result: 200 [{"fruit":"apple","color":"red"},{"fruit":"banana","color":"yellow"}] 2 GET result: 200 {"fruit":"apple","color":"red"} 3 POST result: 200 Item added with id 2 4 PUT result: 200 Item replaced at id 0 5 PATCH result: 200 Item updated at id 0 6 DELETE result: 200 Item deleted
See also:
Good job if you chose Flutter. You write the code once and it works for both Android and iOS. Having already made the client app for Android and iOS, I can tell you that Flutter cuts your time in half.
We will have the following layout:
Add the http
dependency to your pubspec.yaml
file.
1dependencies: 2 http: ^0.12.0+1
Replace main.dart
with the following code:
1import 'dart:io'; 2 import 'package:flutter/material.dart'; 3 import 'package:http/http.dart'; 4 5 void main() => runApp(MyApp()); 6 7 class MyApp extends StatelessWidget { 8 @override 9 Widget build(BuildContext context) { 10 return MaterialApp( 11 debugShowCheckedModeBanner: false, 12 home: Scaffold( 13 appBar: AppBar(title: Text('Client App (Flutter)')), 14 body: BodyWidget(), 15 ), 16 ); 17 } 18 } 19 20 class BodyWidget extends StatelessWidget { 21 @override 22 Widget build(BuildContext context) { 23 return Align( 24 alignment: Alignment.topCenter, 25 child: Padding( 26 padding: const EdgeInsets.only(top: 32.0), 27 child: SizedBox( 28 width: 200, 29 child: Column( 30 crossAxisAlignment: CrossAxisAlignment.stretch, 31 children: <Widget>[ 32 RaisedButton( 33 child: Text('Make GET (all) request'), 34 onPressed: () { 35 _makeGetAllRequest(); 36 }, 37 ), 38 RaisedButton( 39 child: Text('Make GET (one) request'), 40 onPressed: () { 41 _makeGetOneRequest(); 42 }, 43 ), 44 RaisedButton( 45 child: Text('Make POST request'), 46 onPressed: () { 47 _makePostRequest(); 48 }, 49 ), 50 RaisedButton( 51 child: Text('Make PUT request'), 52 onPressed: () { 53 _makePutRequest(); 54 }, 55 ), 56 RaisedButton( 57 child: Text('Make PATCH request'), 58 onPressed: () { 59 _makePatchRequest(); 60 }, 61 ), 62 RaisedButton( 63 child: Text('Make DELETE request'), 64 onPressed: () { 65 _makeDeleteRequest(); 66 }, 67 ), 68 ], 69 ), 70 ), 71 ), 72 ); 73 } 74 75 static const Map<String, String> headers = {"Content-type": "application/json"}; 76 77 // access localhost from the emulator/simulator 78 String _hostname() { 79 if (Platform.isAndroid) 80 return 'http://10.0.2.2:3000'; 81 else 82 return 'http://localhost:3000'; 83 } 84 85 // GET all 86 _makeGetAllRequest() async { 87 // get everything 88 Response response = await get(_hostname()); 89 // examples of info available in response 90 int statusCode = response.statusCode; 91 String jsonString = response.body; 92 print('Status: $statusCode, $jsonString'); 93 } 94 95 // GET one 96 _makeGetOneRequest() async { 97 // only get a single item at index 0 98 int idToGet = 0; 99 String url = '${_hostname()}/$idToGet'; 100 Response response = await get(url); 101 int statusCode = response.statusCode; 102 String jsonString = response.body; 103 print('Status: $statusCode, $jsonString'); 104 } 105 106 // POST 107 _makePostRequest() async { 108 // set up POST request arguments 109 String json = '{"fruit": "pear", "color": "green"}'; 110 // make POST request 111 Response response = await post(_hostname(), headers: headers, body: json); 112 int statusCode = response.statusCode; 113 String body = response.body; 114 print('Status: $statusCode, $body'); 115 } 116 117 // PUT 118 _makePutRequest() async { 119 // set up PUT request arguments 120 int idToReplace = 0; 121 String url = '${_hostname()}/$idToReplace'; 122 String json = '{"fruit": "watermellon", "color": "red and green"}'; 123 // make PUT request 124 Response response = await put(url, headers: headers, body: json); 125 int statusCode = response.statusCode; 126 String body = response.body; 127 print('Status: $statusCode, $body'); 128 } 129 130 // PATCH 131 _makePatchRequest() async { 132 // set up PATCH request arguments 133 int idToUpdate = 0; 134 String url = '${_hostname()}/$idToUpdate'; 135 String json = '{"color": "green"}'; 136 // make PATCH request 137 Response response = await patch(url, headers: headers, body: json); 138 int statusCode = response.statusCode; 139 String body = response.body; 140 print('Status: $statusCode, $body'); 141 } 142 143 // DELETE 144 void _makeDeleteRequest() async { 145 // set up DELETE request argument 146 int idToDelete = 0; 147 String url = '${_hostname()}/$idToDelete'; 148 // make DELETE request 149 Response response = await delete(url); 150 int statusCode = response.statusCode; 151 String body = response.body; 152 print('Status: $statusCode, $body'); 153 } 154 } 155 156 // For help converting JSON to objects in Flutter see 157 // this post https://stackoverflow.com/a/54657953 158 class Fruit { 159 160 int id; 161 String fruit; 162 String color; 163 164 Fruit(this.fruit, this.color); 165 166 // named constructor 167 Fruit.fromJson(Map<String, dynamic> json) 168 : fruit = json['fruit'], 169 color = json['color']; 170 171 // method 172 Map<String, dynamic> toJson() { 173 return { 174 'fruit': fruit, 175 'color': color, 176 }; 177 } 178 }
We used the http
package to make the requests. We get back a Response object from which we can get the status code and body. Although we didn’t use it here, I added a model object at the end that included the code to convert JSON strings to and from Map objects.
With the server that you made in part two running, test the Flutter app in the Android emulator or iOS simulator. Note the log statement that Android Studio prints in the Run tab:
1GET result: 200 [{"fruit":"apple","color":"red"},{"fruit":"banana","color":"yellow"}] 2 GET result: 200 {"fruit":"apple","color":"red"} 3 POST result: 200 Item added with id 2 4 PUT result: 200 Item replaced at id 0 5 PATCH result: 200 Item updated at id 0 6 DELETE result: 200 Item deleted at id 0
We’ve covered a lot in this series. It’s my hope that this will be a solid start to your backend development work. Starting out on a new technology is the most difficult step. It will get easier from here.
The essential files for each of the server and client examples in this series are on GitHub.
You already have a working server. However, the following topics are some things you will want to work on before your server is ready for production.
In the server examples in part two, we used an array as a mock database. Later, of course, you’ll want to add a real database. The “further study” links I included at the end of both server sections tell how to do that.
Modern versions of Android and iOS require secure encrypted connections by default when accessing the internet. We bypassed that security when we made the client apps in above so that we wouldn’t have to bother registering with a certificate authority.
Don’t bypass it in your production apps, though! It’s not a lot more work to set it up on the server and you can get a free certificate from Let’s Encrypt. (I wouldn’t recommend using a self-signed certificate.)
If you put your server online now, anyone in the world could mess your database up. It’s probably fine to leave your GET
methods open to the world as long as there is no sensitive data, but you will surely want to add some sort of authentication for who is allowed to POST
, PUT
, PATCH
, and DELETE
. And even when users are authenticated, never trust what they send you. Validate any data you receive.
Node.js and Server Side Dart both support unit testing. You really need to write tests. The good news with backend programming is that you don’t have the UI to deal with.
When you are ready to deploy the server, you might consider getting a VPS running Linux. This is convenient because getting the server up and running is essentially the same as doing it on your local machine.
I've found quite a few VPSs on LowEndBox for under $20 USD per year. They’re great for learning and even for small production apps. (Every now and then a company goes out of business, though, so keep backups)
In the future when reliability and scalability become more important, you can consider deploying to one of the big-name cloud hosting providers.
It is also recommended to put a reverse proxy between your server app and the internet. Nginx works well for this. See the following links for help: