Becoming a backend developer - Part 2: Building the server
You need experience in at least one of Android, iOS or Flutter development.
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.
- Part 1: Foundational concepts
- Part 2: Building the server
- Part 3: Connecting to the server from a mobile app
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:
// The require() function loads modules in Node.js.
// A module is code that is in another file.
// 'http' is a builtin module in Node.js. It allows data transfer
// using the HTTP protocol and enables setting up an HTTP server.
const http = require('http');
// During development we will use the localhost.
const hostname = '127.0.0.1';
const port = 3000;
// set up an http server
// Two parameters are passed in:
// req = request (from the client)
// res = response (from the server)
const server = http.createServer((req, res) => {
// A status code of 200 means OK (A 404 would mean Not Found)
res.statusCode = 200;
// A header adds additional information.
// Here we are using a name-value pair to set the
// media type (MIME type) as plain text (as opposed to html).
res.setHeader('Content-Type', 'text/plain');
// This writes a message and then ends the response.
res.end('Hello World\n');
});
// This causes the server to listen for requests from clients on
// the hostname and port that we defined above.
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
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:
GET http://localhost:3000/ // get all items
GET http://localhost:3000/id // get item at id
POST http://localhost:3000/ // create new item
PUT http://localhost:3000/id // replace item at id
PATCH http://localhost:3000/id // update item at id
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.
mkdir nodejs_server
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:
// nodejs_server/index.js
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
// bodyParser is a type of middleware
// It helps convert JSON strings
// the 'use' method assigns a middleware
app.use(bodyParser.json({ type: 'application/json' }));
const hostname = '127.0.0.1';
const port = 3000;
// http status codes
const statusOK = 200;
const statusNotFound = 404;
// using an array to simulate a database for demonstration purposes
var mockDatabase = [
{
fruit: "apple",
color: "red"
},
{
fruit: "banana",
color: "yellow"
}
]
// Handle GET (all) request
app.get('/', function(req, res) {
// error checking
if (mockDatabase.length < 1) {
res.statusCode = statusNotFound;
res.send('Item not found');
return;
}
// send response
res.statusCode = statusOK;
res.send(mockDatabase);
});
// Handle GET (one) request
app.get('/:id', function(req, res) {
// error checking
var id = req.params.id;
if (id < 0 || id >= mockDatabase.length) {
res.statusCode = statusNotFound;
res.send('Item not found');
return;
}
// send response
res.statusCode = statusOK;
res.send(mockDatabase[id]);
});
// Handle POST request
app.post('/', function(req, res) {
// get data from request
var newObject = req.body; // TODO validate data
mockDatabase.push(newObject);
// send created item back with id included
var id = mockDatabase.length - 1;
res.statusCode = statusOK;
res.send(`Item added with id ${id}`);
});
// Handle PUT request
app.put('/:id', function(req, res) {
// replace current object
var id = req.params.id; // TODO validate id
var replacement = req.body; // TODO validate data
mockDatabase[id] = replacement;
// report back to the client
res.statusCode = statusOK;
res.send(`Item replaced at id ${id}`);
});
// Handle PATCH request
app.patch('/:id', function(req, res) {
// update current object
var id = req.params.id; // TODO validate id
var newColor = req.body.color; // TODO validate data
mockDatabase[id].color = newColor;
// report back to the client
res.statusCode = statusOK;
res.send(`Item updated at id ${id}`);
});
// Handle DELETE request
app.delete('/:id', function(req, res) {
// delete specified item
var id = req.params.id; // TODO validate id
mockDatabase.splice(id, 1);
// send response back
res.statusCode = statusOK;
res.send(`Item deleted at id ${id}`);
});
app.listen(port, hostname, function () {
console.log(`Listening at http://${hostname}:${port}/...`);
});
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.
Further study
- NodeJS REST API
- Building a RESTful API with Node.js
- Node.js Crash Course
- Building a Node.js REST API with Express
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:
# Linux or Mac
echo $PATH
# Windows Command Prompt
echo %path%
# Windows Powershell
$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:
# Linux or Mac
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:
GET http://localhost:3000/ // get all items
GET http://localhost:3000/id // get item at id
POST http://localhost:3000/ // create new item
PUT http://localhost:3000/id // replace item at id
PATCH http://localhost:3000/id // update item at id
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:
// dart_server/lib/channel.dart
import 'package:dart_server/controller.dart';
import 'dart_server.dart';
// This class sets up the controller that will handle our HTTP requests
class DartServerChannel extends ApplicationChannel {
Future prepare() async {
// auto generated code
logger.onRecord.listen((rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}"));
}
Controller get entryPoint {
final router = Router();
// We are only setting up one route.
// We could add more below if we had them.
// A route refers to the path portion of the URL.
router
.route('/[:id]') // the root path with an optional id variable
.link(() => MyController()); // requests are forwarded to our controller
return router;
}
}
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:
// dart_server/lib/controller.dart
import 'dart:async';
import 'dart:io';
import 'package:aqueduct/aqueduct.dart';
// using a list to simulate a database for demonstration purposes
List<Map<String, dynamic>> mockDatabase = [
{
'fruit': 'apple',
'color': 'red'
},
{
'fruit': 'banana',
'color': 'yellow'
}
];
class MyController extends ResourceController {
// Handle GET (all) request
.get()
Future<Response> getAllFruit() async {
// return the whole list
return Response.ok(mockDatabase);
}
// Handle GET (one) request
.get('id')
Future<Response> getFruitByID(.path('id') int id) async {
// error checking
if (id < 0 || id >= mockDatabase.length){
return Response.notFound(body: 'Item not found');
}
// return json for item at id
return Response.ok(mockDatabase[id]);
}
// Handle POST request
.post()
Future<Response> addFruit() async {
// get json from request
final Map<String, dynamic> item = await request.body.decode(); // TODO validate
// create item (TODO return error status code if there was a problem)
mockDatabase.add(item);
// report back to client
final int id = mockDatabase.length - 1;
return Response.ok('Item added with id $id');
}
// Handle PUT request
.put('id')
Future<Response> putContent(.path('id') int id) async {
// error checking
if (id < 0 || id >= mockDatabase.length){
return Response.notFound(body: 'Item not found');
}
// get the updated item from the client
final Map<String, dynamic> item = await request.body.decode(); // TODO validate
// make the update
mockDatabase[id] = item;
// report back to the client
return Response.ok('Item replaced at id $id');
}
// Handle PATCH request
// (PATCH does not have its own @Operation method so
// the constructor parameter is used.)
('PATCH', 'id')
Future<Response> patchContent(.path('id') int id) async {
// error checking
if (id < 0 || id >= mockDatabase.length){
return Response.notFound(body: 'Item not found');
}
// get the updated item from the client
final Map<String, dynamic> item = await request.body.decode(); // TODO validate
// make the partial update
mockDatabase\[id\]['color'] = item['color'];
// report back to the client
return Response.ok('Item updated at id $id');
}
// Handle DELETE request
.delete('id')
Future<Response> deleteContent(.path('id') int id) async {
// error checking
if (id < 0 || id >= mockDatabase.length){
return Response.notFound(body: 'Item not found');
}
// do the delete
mockDatabase.removeAt(id);
// report back to the client
return Response.ok('Item deleted at id $id');
}
}
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.
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.
class Fruit {
Fruit(this.fruit, this.color);
// named constructor
Fruit.fromJson(Map<String, dynamic> json)
: fruit = json['fruit'] as String,
color = json['color'] as String;
int id;
String fruit;
String color;
// method
Map<String, dynamic> toJson() {
return {
'fruit': fruit,
'color': color,
};
}
}
Further study
- Build RESTful Web APIs with Dart, Aqueduct and PostgreSQL #2: Routing and Controllers
- Dart: First API And Create Controller | 1/7 | Aqueduct | Backend Course
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.
22 March 2019
by Suragch