Becoming a backend developer - Part 2: Building the server

Introduction

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.

Introduction

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!

Prerequisites

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.

Building the backend

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.

Backend example one: Node.js

Note: This tutorial was tested with Node.js 10.15.1

Install Node.js

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.

Implement our API

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:

backend-mobile-2-1

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.

backend-mobile-2-2

Further study

Backend example two: Server Side Dart

Note: This tutorial was tested with Dart 2.1.2

Install Dart

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.

Install Aqueduct

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.

Implement our API

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:

backend-mobile-2-1

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.

Testing the server

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.

backend-mobile-2-3

Supplemental code

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    }

Further study

Conclusion

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.