Becoming a backend developer - Part 3: Connecting to the server from a mobile app

Introduction

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.

Introduction

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.

backend-mobile-1-2

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.

Prerequisites

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:

  • Android: Android Studio 3.3
  • iOS: Xcode 10.1
  • Flutter: Android Studio 3.3 with Flutter 1.2.1

Android client app

Create a layout similar to the image below (see source code):

backend-mobile-3-1

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

iOS client app

Create a layout similar to the image below:

backend-mobile-3-2

In the Info.plist file, add the following key:

1<key>NSAppTransportSecurity</key>
2    <dict>
3        <key>NSAllowsArbitraryLoads</key>
4        <true/>
5    </dict>
backend-mobile-3-3

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:

Flutter client app

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:

backend-mobile-3-4

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

Conclusion

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.

Going on

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.

Database

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.

HTTPS

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.)

Authentication, validation, and testing

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.

Publishing

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: