Becoming a backend developer - Part 3: Connecting to the server from a mobile app
You need experience in at least one of Android, iOS or Flutter development.
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.
- Part 1: Foundational concepts
- Part 2: Building the server
- Part 3: Connecting to the server from a mobile app
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.
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):
In the manifest add the INTERNET
permission (see source code):
<uses-permission android:name="android.permission.INTERNET" />
And allow clear text:
<application
android:usesCleartextTraffic="true"
...
>
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):
package com.example.backendclient
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.os.AsyncTask
import android.util.Log
import android.view.View
import java.io.*
import java.net.HttpURLConnection
import java.net.URL
private const val HOST = "http://10.0.2.2:3000"
private const val TAG = "TAG"
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun makeGetAllRequest(view: View) {
HttpGetRequest().execute(HOST)
}
fun makeGetOneRequest(view: View) {
val idToGet = 0
val url = "$HOST/$idToGet"
HttpGetRequest().execute(url)
}
fun makePostRequest(view: View) {
val json = "{\"fruit\": \"pear\", \"color\": \"green\"}"
HttpPostRequest().execute(HOST, json)
}
fun makePutRequest(view: View) {
val idToReplace = 0
val url = "$HOST/$idToReplace"
val json = "{\"fruit\": \"watermellon\", \"color\": \"red and green\"}"
HttpPutRequest().execute(url, json)
}
fun makePatchRequest(view: View) {
val idToUpdate = 0
val url = "$HOST/$idToUpdate"
val json = "{\"color\": \"green\"}"
HttpPatchRequest().execute(url, json)
}
fun makeDeleteRequest(view: View) {
val idToDelete = 0
val url = "$HOST/$idToDelete"
HttpDeleteRequest().execute(url)
}
// GET
class HttpGetRequest : AsyncTask<String, Void, Void>() {
override fun doInBackground(vararg params: String): Void? {
val urlString = params[0]
val myUrl = URL(urlString)
val connection = myUrl.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
val result = getStringFromInputStream(connection.inputStream)
val statusCode = connection.responseCode
connection.disconnect()
Log.i(TAG, "GET result: $statusCode $result")
return null
}
}
// POST
class HttpPostRequest : AsyncTask<String, Void, Void>() {
override fun doInBackground(vararg params: String): Void? {
val urlString = params[0]
val json = params[1]
val myUrl = URL(urlString)
val connection = myUrl.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/json")
writeStringToOutputStream(json, connection.outputStream)
val result = getStringFromInputStream(connection.inputStream)
val statusCode = connection.responseCode
connection.disconnect()
Log.i(TAG, "POST result: $statusCode $result")
return null
}
}
// PUT
class HttpPutRequest : AsyncTask<String, Void, Void>() {
override fun doInBackground(vararg params: String): Void? {
val urlString = params[0]
val json = params[1]
val myUrl = URL(urlString)
val connection = myUrl.openConnection() as HttpURLConnection
connection.requestMethod = "PUT"
connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/json")
writeStringToOutputStream(json, connection.outputStream)
val result = getStringFromInputStream(connection.inputStream)
val statusCode = connection.responseCode
connection.disconnect()
Log.i(TAG, "PUT result: $statusCode $result")
return null
}
}
// PATCH
class HttpPatchRequest : AsyncTask<String, Void, Void>() {
override fun doInBackground(vararg params: String): Void? {
val urlString = params[0]
val json = params[1]
val myUrl = URL(urlString)
val connection = myUrl.openConnection() as HttpURLConnection
connection.requestMethod = "PATCH"
connection.doOutput = true
connection.setRequestProperty("Content-Type", "application/json")
writeStringToOutputStream(json, connection.outputStream)
val result = getStringFromInputStream(connection.inputStream)
val statusCode = connection.responseCode
connection.disconnect()
Log.i(TAG, "PATCH result: $statusCode $result")
return null
}
}
// DELETE
class HttpDeleteRequest : AsyncTask<String, Void, Void>() {
override fun doInBackground(vararg params: String): Void? {
val urlString = params[0]
val myUrl = URL(urlString)
val connection = myUrl.openConnection() as HttpURLConnection
connection.requestMethod = "DELETE"
val result = getStringFromInputStream(connection.inputStream)
val statusCode = connection.responseCode
connection.disconnect()
Log.i(TAG, "DELETE result: $statusCode $result")
return null
}
}
}
private fun writeStringToOutputStream(json: String, outputStream: OutputStream) {
val bytes = json.toByteArray(charset("UTF-8")) // API 19: StandardCharsets.UTF_8
outputStream.write(bytes)
outputStream.close()
}
private fun getStringFromInputStream(stream: InputStream): String {
val text = stream.bufferedReader().use { it.readText() }
stream.close()
return text
}
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:
GET result: 200 [{"fruit":"apple","color":"red"},{"fruit":"banana","color":"yellow"}]
GET result: 200 {"fruit":"apple","color":"red"}
POST result: 200 Item added with id 2
PUT result: 200 Item replaced at id 0
PATCH result: 200 Item updated at id 0
DELETE result: 200 Item deleted at id 0
iOS client app
Create a layout similar to the image below:
In the Info.plist file, add the following key:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</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:
import UIKit
class ViewController: UIViewController {
let host = "http://localhost:3000"
// make GET (all) request
@IBAction func makeGetAllRequestTapped(_ sender: UIButton) {
guard let url = URL(string: host) else {return}
// background task to make request with URLSession
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let statusCode = (response as? HTTPURLResponse)?.statusCode
else {return}
guard let body = data
else {return}
guard let jsonString = String(data: body, encoding: String.Encoding.utf8)
else {return}
print("GET result: \(statusCode) \(jsonString)")
}
// start the task
task.resume()
}
// make GET (one) request
@IBAction func makeGetOneRequestTapped(_ sender: UIButton) {
let idToGet = 0;
let urlString = "\(host)/\(idToGet)"
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let statusCode = (response as? HTTPURLResponse)?.statusCode
else {return}
guard let body = data
else {return}
guard let jsonString = String(data: body, encoding: String.Encoding.utf8)
else {return}
print("GET result: \(statusCode) \(jsonString)")
}
task.resume()
}
// make POST request
@IBAction func makePostRequestTapped(_ sender: UIButton) {
let dictionary = ["fruit" : "pear", "color" : "green"]
// prepare request
guard let url = URL(string: host) else {return}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let json = try? JSONSerialization.data(
withJSONObject: dictionary, options: [])
else {return}
request.httpBody = json
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
guard let statusCode = (response as? HTTPURLResponse)?.statusCode
else {return}
guard let body = data
else {return}
guard let responseString = String(data: body, encoding: .utf8)
else {return}
print("POST result: \(statusCode) \(responseString)")
// If your API returns JSON you could do the following:
// guard let jsonString = try? JSONSerialization.jsonObject(
// with: body, options: []) else {return}
}
task.resume()
}
// make PUT request
@IBAction func makePutRequestTapped(_ sender: UIButton) {
let dictionary = ["fruit" : "watermellon", "color" : "red and green"]
let idToPut = 0;
let urlString = "\(host)/\(idToPut)"
// prepare request
guard let url = URL(string: urlString) else {return}
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let json = try? JSONSerialization.data(
withJSONObject: dictionary, options: [])
else {return}
request.httpBody = json
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
guard let statusCode = (response as? HTTPURLResponse)?.statusCode
else {return}
guard let body = data
else {return}
guard let responseString = String(data: body, encoding: .utf8)
else {return}
print("PUT result: \(statusCode) \(responseString)")
}
task.resume()
}
// make PATCH request
@IBAction func makePatchRequestTapped(_ sender: UIButton) {
let dictionary = ["color" : "green"]
let idToPatch = 0;
let urlString = "\(host)/\(idToPatch)"
// prepare request
guard let url = URL(string: urlString) else {return}
var request = URLRequest(url: url)
request.httpMethod = "PATCH"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let json = try? JSONSerialization.data(
withJSONObject: dictionary, options: [])
else {return}
request.httpBody = json
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
guard let statusCode = (response as? HTTPURLResponse)?.statusCode
else {return}
guard let body = data
else {return}
guard let responseString = String(data: body, encoding: .utf8)
else {return}
print("PATCH result: \(statusCode) \(responseString)")
}
task.resume()
}
// make DELETE request
@IBAction func makeDeleteRequestTapped(_ sender: UIButton) {
let idToDelete = 0;
let urlString = "\(host)/\(idToDelete)"
// prepare request
guard let url = URL(string: urlString) else {return}
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
let task = URLSession.shared.dataTask(with: request) {
(data, response, error) in
guard let statusCode = (response as? HTTPURLResponse)?.statusCode
else {return}
guard let body = data
else {return}
guard let responseString = String(data: body, encoding: .utf8)
else {return}
print("DELETE result: \(statusCode) \(responseString)")
}
task.resume()
}
}
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:
GET result: 200 [{"fruit":"apple","color":"red"},{"fruit":"banana","color":"yellow"}]
GET result: 200 {"fruit":"apple","color":"red"}
POST result: 200 Item added with id 2
PUT result: 200 Item replaced at id 0
PATCH result: 200 Item updated at id 0
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:
Add the http
dependency to your pubspec.yaml
file.
dependencies:
http: ^0.12.0+1
Replace main.dart
with the following code:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: Text('Client App (Flutter)')),
body: BodyWidget(),
),
);
}
}
class BodyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.only(top: 32.0),
child: SizedBox(
width: 200,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
RaisedButton(
child: Text('Make GET (all) request'),
onPressed: () {
_makeGetAllRequest();
},
),
RaisedButton(
child: Text('Make GET (one) request'),
onPressed: () {
_makeGetOneRequest();
},
),
RaisedButton(
child: Text('Make POST request'),
onPressed: () {
_makePostRequest();
},
),
RaisedButton(
child: Text('Make PUT request'),
onPressed: () {
_makePutRequest();
},
),
RaisedButton(
child: Text('Make PATCH request'),
onPressed: () {
_makePatchRequest();
},
),
RaisedButton(
child: Text('Make DELETE request'),
onPressed: () {
_makeDeleteRequest();
},
),
],
),
),
),
);
}
static const Map<String, String> headers = {"Content-type": "application/json"};
// access localhost from the emulator/simulator
String _hostname() {
if (Platform.isAndroid)
return 'http://10.0.2.2:3000';
else
return 'http://localhost:3000';
}
// GET all
_makeGetAllRequest() async {
// get everything
Response response = await get(_hostname());
// examples of info available in response
int statusCode = response.statusCode;
String jsonString = response.body;
print('Status: $statusCode, $jsonString');
}
// GET one
_makeGetOneRequest() async {
// only get a single item at index 0
int idToGet = 0;
String url = '${_hostname()}/$idToGet';
Response response = await get(url);
int statusCode = response.statusCode;
String jsonString = response.body;
print('Status: $statusCode, $jsonString');
}
// POST
_makePostRequest() async {
// set up POST request arguments
String json = '{"fruit": "pear", "color": "green"}';
// make POST request
Response response = await post(_hostname(), headers: headers, body: json);
int statusCode = response.statusCode;
String body = response.body;
print('Status: $statusCode, $body');
}
// PUT
_makePutRequest() async {
// set up PUT request arguments
int idToReplace = 0;
String url = '${_hostname()}/$idToReplace';
String json = '{"fruit": "watermellon", "color": "red and green"}';
// make PUT request
Response response = await put(url, headers: headers, body: json);
int statusCode = response.statusCode;
String body = response.body;
print('Status: $statusCode, $body');
}
// PATCH
_makePatchRequest() async {
// set up PATCH request arguments
int idToUpdate = 0;
String url = '${_hostname()}/$idToUpdate';
String json = '{"color": "green"}';
// make PATCH request
Response response = await patch(url, headers: headers, body: json);
int statusCode = response.statusCode;
String body = response.body;
print('Status: $statusCode, $body');
}
// DELETE
void _makeDeleteRequest() async {
// set up DELETE request argument
int idToDelete = 0;
String url = '${_hostname()}/$idToDelete';
// make DELETE request
Response response = await delete(url);
int statusCode = response.statusCode;
String body = response.body;
print('Status: $statusCode, $body');
}
}
// For help converting JSON to objects in Flutter see
// this post https://stackoverflow.com/a/54657953
class Fruit {
int id;
String fruit;
String color;
Fruit(this.fruit, this.color);
// named constructor
Fruit.fromJson(Map<String, dynamic> json)
: fruit = json['fruit'],
color = json['color'];
// method
Map<String, dynamic> toJson() {
return {
'fruit': fruit,
'color': color,
};
}
}
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:
GET result: 200 [{"fruit":"apple","color":"red"},{"fruit":"banana","color":"yellow"}]
GET result: 200 {"fruit":"apple","color":"red"}
POST result: 200 Item added with id 2
PUT result: 200 Item replaced at id 0
PATCH result: 200 Item updated at id 0
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:
28 March 2019
by Suragch