This is a guide for mobile app developers. In this three-part series we are covering all the basics of what it takes to become a backend developer.
In the first tutorial we went over background knowledge related to client-server communication. This included URIs, HTTP, REST APIs, and JSON. If any of these topics are unfamiliar to you, go back and review them.
Now, let’s build a server!
I'm not assuming you have any backend server experience, but I won't be repeating online documentation, so you will need to be able to following the installation directions from the links I give you.
I will be using Visual Studio Code to do the server programming. Feel free to use another IDE if you prefer.
For development purposes, we are going to install the server software on our local machine and have it communicate directly with our mobile app running in an emulator.
Below are two examples of a backend server: Node.js and Server-Side Dart. You only need to choose one. Node.js is very popular and you write the server code in JavaScript. Server Side Dart is not nearly as popular as Node.js, but for Flutter developers the advantage is that you can use the same language for the frontend and the backend. It really doesn't matter which one you choose, but if you can't decide, go with Node.js.
Note: This tutorial was tested with Node.js 10.15.1
Go to nodejs.org to download and install Node.js.
The Getting Started Guide shows the code for a basic HTTP server. It doesn’t explain the code, though, so I have added comments below:
1// The require() function loads modules in Node.js. 2 // A module is code that is in another file. 3 // 'http' is a builtin module in Node.js. It allows data transfer 4 // using the HTTP protocol and enables setting up an HTTP server. 5 const http = require('http'); 6 7 // During development we will use the localhost. 8 const hostname = '127.0.0.1'; 9 const port = 3000; 10 11 // set up an http server 12 // Two parameters are passed in: 13 // req = request (from the client) 14 // res = response (from the server) 15 const server = http.createServer((req, res) => { 16 17 // A status code of 200 means OK (A 404 would mean Not Found) 18 res.statusCode = 200; 19 20 // A header adds additional information. 21 // Here we are using a name-value pair to set the 22 // media type (MIME type) as plain text (as opposed to html). 23 res.setHeader('Content-Type', 'text/plain'); 24 25 // This writes a message and then ends the response. 26 res.end('Hello World\n'); 27 }); 28 29 // This causes the server to listen for requests from clients on 30 // the hostname and port that we defined above. 31 server.listen(port, hostname, () => { 32 console.log(`Server running at http://${hostname}:${port}/`); 33 });
Go ahead and test the code by running the app as described in the Getting Started Guide. You should see “Hello World” in the browser window when you navigate to http://localhost:3000.
In Part 1 [ADD LINK] I told you the REST API that we were going to make would look like this:
1GET http://localhost:3000/ // get all items 2 GET http://localhost:3000/id // get item at id 3 POST http://localhost:3000/ // create new item 4 PUT http://localhost:3000/id // replace item at id 5 PATCH http://localhost:3000/id // update item at id 6 DELETE http://localhost:3000/id // delete item at id
Our client app that we will make in Part 3 is going to look like this:
So we need to make our server handle all of these requests. First go to the terminal and create a new directory for our Node.js project.
1mkdir nodejs_server 2 cd nodejs_server
The way to create a new project is to use the Node Package Manager (npm). Run the following command and accept the default values for everything. (If you need to edit this info later you can open package.json
.)
npm init
We are also going to use the Express framework, which simplifies a lot of the HTTP protocol handling.
npm install express --save
Now create the server file that we named in the npm init
step above:
touch index.js
Open it in an editor (I'm using VSCode), and paste in the following:
1// nodejs_server/index.js 2 3 var express = require('express'); 4 var bodyParser = require('body-parser'); 5 var app = express(); 6 7 // bodyParser is a type of middleware 8 // It helps convert JSON strings 9 // the 'use' method assigns a middleware 10 app.use(bodyParser.json({ type: 'application/json' })); 11 12 const hostname = '127.0.0.1'; 13 const port = 3000; 14 15 // http status codes 16 const statusOK = 200; 17 const statusNotFound = 404; 18 19 // using an array to simulate a database for demonstration purposes 20 var mockDatabase = [ 21 { 22 fruit: "apple", 23 color: "red" 24 }, 25 { 26 fruit: "banana", 27 color: "yellow" 28 } 29 ] 30 31 // Handle GET (all) request 32 app.get('/', function(req, res) { 33 // error checking 34 if (mockDatabase.length < 1) { 35 res.statusCode = statusNotFound; 36 res.send('Item not found'); 37 return; 38 } 39 // send response 40 res.statusCode = statusOK; 41 res.send(mockDatabase); 42 }); 43 44 // Handle GET (one) request 45 app.get('/:id', function(req, res) { 46 // error checking 47 var id = req.params.id; 48 if (id < 0 || id >= mockDatabase.length) { 49 res.statusCode = statusNotFound; 50 res.send('Item not found'); 51 return; 52 } 53 // send response 54 res.statusCode = statusOK; 55 res.send(mockDatabase[id]); 56 }); 57 58 // Handle POST request 59 app.post('/', function(req, res) { 60 // get data from request 61 var newObject = req.body; // TODO validate data 62 mockDatabase.push(newObject); 63 // send created item back with id included 64 var id = mockDatabase.length - 1; 65 res.statusCode = statusOK; 66 res.send(`Item added with id ${id}`); 67 }); 68 69 // Handle PUT request 70 app.put('/:id', function(req, res) { 71 // replace current object 72 var id = req.params.id; // TODO validate id 73 var replacement = req.body; // TODO validate data 74 mockDatabase[id] = replacement; 75 // report back to the client 76 res.statusCode = statusOK; 77 res.send(`Item replaced at id ${id}`); 78 }); 79 80 // Handle PATCH request 81 app.patch('/:id', function(req, res) { 82 // update current object 83 var id = req.params.id; // TODO validate id 84 var newColor = req.body.color; // TODO validate data 85 mockDatabase[id].color = newColor; 86 // report back to the client 87 res.statusCode = statusOK; 88 res.send(`Item updated at id ${id}`); 89 }); 90 91 // Handle DELETE request 92 app.delete('/:id', function(req, res) { 93 // delete specified item 94 var id = req.params.id; // TODO validate id 95 mockDatabase.splice(id, 1); 96 // send response back 97 res.statusCode = statusOK; 98 res.send(`Item deleted at id ${id}`); 99 }); 100 101 app.listen(port, hostname, function () { 102 console.log(`Listening at http://${hostname}:${port}/...`); 103 });
Save the file and run it in the terminal.
node index.js
The server is now running on your machine. You can use Postman (see docs and tutorial) to test the server now, or you can use one of the client apps that we will make in part three.
Note: This tutorial was tested with Dart 2.1.2
If you have Flutter installed on your system, then Dart is already installed. But if not, then go to this link to download and install the Dart SDK.
Check if dart/bin
is in your system path:
1# Linux or Mac 2 echo $PATH 3 4 # Windows Command Prompt 5 echo %path% 6 7 # Windows Powershell 8 $env:Path -split ';'
If it isn't and you just installed it from the link above (because you don't have Flutter), you can add it to your path like this:
1# Linux or Mac 2 export PATH="$PATH:/usr/lib/dart/bin"
On Windows it is easiest to use the GUI to set environment variables.
If you already had Flutter/Dart installed, find your Flutter SDK directory. Then you can add the path like this (replacing <flutter>
):
export PATH="$PATH:<flutter>/bin/cache/dart-sdk/bin"
This only updates the path until you restart your machine. You will probably want to update your .bash_profile
(or whatever you use on your system) to make it permanent.
We are also going to use the Aqueduct framework to make HTTP request APIs easier to build. Now that we have Dart installed, we can install Aqueduct like this:
pub global activate aqueduct
Follow the directions to add the $HOME/.pub-cache/bin
to your path if you are instructed to.
In part one I told you the REST API that we were going to make would look like this:
1GET http://localhost:3000/ // get all items 2 GET http://localhost:3000/id // get item at id 3 POST http://localhost:3000/ // create new item 4 PUT http://localhost:3000/id // replace item at id 5 PATCH http://localhost:3000/id // update item at id 6 DELETE http://localhost:3000/id // delete item at id
Our client app that we will make in part three is going to look like this:
So we need to make our server handle all of these requests.
First go to the terminal and cd to the directory that you want to create the server project folder in. Then type:
aqueduct create dart_server
Open the project in an editor. The Aqueduct documentation recommends IntelliJ IDEA, but I am using Visual Studio Code with the Dart plugin.
Open the lib/channel.dart
file and replace it with the following code:
1// dart_server/lib/channel.dart 2 3 import 'package:dart_server/controller.dart'; 4 import 'dart_server.dart'; 5 6 // This class sets up the controller that will handle our HTTP requests 7 class DartServerChannel extends ApplicationChannel { 8 9 @override 10 Future prepare() async { 11 // auto generated code 12 logger.onRecord.listen((rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}")); 13 } 14 15 @override 16 Controller get entryPoint { 17 final router = Router(); 18 19 // We are only setting up one route. 20 // We could add more below if we had them. 21 // A route refers to the path portion of the URL. 22 router 23 .route('/[:id]') // the root path with an optional id variable 24 .link(() => MyController()); // requests are forwarded to our controller 25 return router; 26 } 27 }
In the comments I talked about a controller. Let's make that now. Create a file called controller.dart
in the lib/
directory. Paste in the code below to handle HTTP requests:
1// dart_server/lib/controller.dart 2 3 import 'dart:async'; 4 import 'dart:io'; 5 import 'package:aqueduct/aqueduct.dart'; 6 7 // using a list to simulate a database for demonstration purposes 8 List<Map<String, dynamic>> mockDatabase = [ 9 { 10 'fruit': 'apple', 11 'color': 'red' 12 }, 13 { 14 'fruit': 'banana', 15 'color': 'yellow' 16 } 17 ]; 18 19 class MyController extends ResourceController { 20 21 // Handle GET (all) request 22 @Operation.get() 23 Future<Response> getAllFruit() async { 24 // return the whole list 25 return Response.ok(mockDatabase); 26 } 27 28 // Handle GET (one) request 29 @Operation.get('id') 30 Future<Response> getFruitByID(@Bind.path('id') int id) async { 31 // error checking 32 if (id < 0 || id >= mockDatabase.length){ 33 return Response.notFound(body: 'Item not found'); 34 } 35 // return json for item at id 36 return Response.ok(mockDatabase[id]); 37 } 38 39 // Handle POST request 40 @Operation.post() 41 Future<Response> addFruit() async { 42 // get json from request 43 final Map<String, dynamic> item = await request.body.decode(); // TODO validate 44 // create item (TODO return error status code if there was a problem) 45 mockDatabase.add(item); 46 // report back to client 47 final int id = mockDatabase.length - 1; 48 return Response.ok('Item added with id $id'); 49 } 50 51 // Handle PUT request 52 @Operation.put('id') 53 Future<Response> putContent(@Bind.path('id') int id) async { 54 // error checking 55 if (id < 0 || id >= mockDatabase.length){ 56 return Response.notFound(body: 'Item not found'); 57 } 58 // get the updated item from the client 59 final Map<String, dynamic> item = await request.body.decode(); // TODO validate 60 // make the update 61 mockDatabase[id] = item; 62 // report back to the client 63 return Response.ok('Item replaced at id $id'); 64 } 65 66 // Handle PATCH request 67 // (PATCH does not have its own @Operation method so 68 // the constructor parameter is used.) 69 @Operation('PATCH', 'id') 70 Future<Response> patchContent(@Bind.path('id') int id) async { 71 // error checking 72 if (id < 0 || id >= mockDatabase.length){ 73 return Response.notFound(body: 'Item not found'); 74 } 75 // get the updated item from the client 76 final Map<String, dynamic> item = await request.body.decode(); // TODO validate 77 // make the partial update 78 mockDatabase\[id\]['color'] = item['color']; 79 // report back to the client 80 return Response.ok('Item updated at id $id'); 81 } 82 83 // Handle DELETE request 84 @Operation.delete('id') 85 Future<Response> deleteContent(@Bind.path('id') int id) async { 86 // error checking 87 if (id < 0 || id >= mockDatabase.length){ 88 return Response.notFound(body: 'Item not found'); 89 } 90 // do the delete 91 mockDatabase.removeAt(id); 92 // report back to the client 93 return Response.ok('Item deleted at id $id'); 94 } 95 }
Save your changes.
Make sure you are inside the root of your project folder:
cd dart_server
Normally you would start the server in the terminal like this:
aqueduct serve
However, Aqueduct defaults to listening on port 8888. In all of the client apps and Node.js we are using port 3000, so let’s do that here, too. I'm also limiting the number of server instances (aka isolates) to one. This is only because we are using a mock database. For your production server with a real database, you can let the server choose the number of isolates to run. So start the server like this:
aqueduct serve --port 3000 --isolates 1
The server is now running on your machine. You can use Postman (see docs and tutorial) to test the server now, or you can use one of the client apps that we will make in part three.
The following is a model class that includes code to do JSON conversions. I’m including it here for your reference, but you don’t need to do anything with it today. For more help converting JSON to objects in Dart see this post.
1class Fruit { 2 3 Fruit(this.fruit, this.color); 4 5 // named constructor 6 Fruit.fromJson(Map<String, dynamic> json) 7 : fruit = json['fruit'] as String, 8 color = json['color'] as String; 9 10 int id; 11 String fruit; 12 String color; 13 14 // method 15 Map<String, dynamic> toJson() { 16 return { 17 'fruit': fruit, 18 'color': color, 19 }; 20 } 21 }
In this tutorial, we saw two ways to create a backend server. Although the details were somewhat different, the REST API that we implemented was exactly the same. If you don’t like Node.js or Server Side Dart, there are many more to choose from. (I was playing around with Server Side Swift for a while before I decided to pursue Flutter for the frontend.) Whatever server technology you choose, just implement the REST API that we used here.
You can find the server code for this lesson on GitHub.
In the last part of this tutorial we will learn how to make Android, iOS, and Flutter client apps that are able to communicate with the servers that we made in this lesson.