In order to develop our Single Page Application, we need to create a back-end service. For consistency, we're going to use a Node.js server with sierra, an MVC framework.

Let's create a new project for our service.

First, we need to ensure Yeoman is installed

npm install -g yo

Then we need to install our Yeoman generator for TypeScript

npm install -g generator-typescript-project

Now we are ready to create our new project. In our projects folder, we will create a directory example-service.

mkdir example-service

Enter the new directory

cd example-service

Now let's run our generator, and answer any prompts.

yo typescript-project

Preparing our Project

Let's install our dependencies. First, we need to install sierra.

npm install sierra

Open src/tests/test.ts and remove any content.

Open tsconfig.json and change compilerOptions.target to es2017.

Creating a Sierra Server

Then open src/scripts/main.ts. Remove any content, add the following lines.

import Sierra from 'sierra';

let sierra = new Sierra();

sierra.init();
sierra.listen(8001);

Note that we have imported sierra. Then we created a Sierra instance, initialized it, and started listening on port 8001.

Let's go ahead and run the following in the command prompt.

tsc
npm run node

Open your browser, and navigate to http://localhost:8001. You should see the message "no route found".

This means that Sierra is running correctly, but we haven't yet configured it to do anything.

Middleware

Sierra works by running a series of "Middleware", which are simply a set of functions which are run in order, every time a Request is received by the server. Middleware handle common tasks like retrieving Session information, parsing Post Body data, checking Authentication, etc. Each Middleware returns a Promise, which allows them to pause execution in case of Database calls and the like. If any unhandled exceptions occur, they are handled by the Error Middleware. Once all of the Middleware have run, execution is sent to the "Router" which parses the URL, and runs a specified function.

Let's take a look at how this all works.

In your src/scripts/main.ts, update the import line to:

import Sierra, { BodyMiddleware } from 'sierra';

This brings in the BodyMiddleware. Now let's add it to our Middleware stack.

After let sierra = new Sierra(); add the line

sierra.use(BodyMiddleware.handle);

This will cause our BodyMiddleware to check for any Post Body data, and assemble it for use.

Routing

If you build and run our server again, you'll see it still doesn't do anything. We need to set up routes.

Create a file src/scripts/controllers/HomeController.ts.

Inside the file, add the lines:

import { Controller, method } from 'sierra';

export default class HomeController extends Controller {

    constructor() {
        super('/');
    }

    @method('get')
    async index() {
        return 'Home/index';
    }
}

Note that we extend Controller, which provides a basic container for routes. In the constructor, we set the base of our route to '/', which means it will be our default Controller. We'll take a look at auto-generated route names later.

Then we created a "Controller method" called index by adding the @method decorator and passing 'get'. This means our route will only respond to the GET HTTP method.

We also named the method index which is a reserved method name. It means this is the default method, and since we are already using the default controller, it means this method will run if we run the default URL. Hence, we have just created a method for http://localhost:8001.

Now, let's add our controller to Sierra.

Open src/scripts/main.ts and import our HomeController:

import HomeController from './controllers/HomeController';

Now, before the init call, add HomeController to sierra:

sierra.addController(new HomeController());

Build and run our server, and again navigate to http://localhost:8001. You should see the message "Home/index".

Gateway

Before we can build a full RESTful API, we need some data to serve. For simplicity, we're going to use a local, JavaScript only database. For a production server, this database would likely be insufficient, but it will work great for our purposes.

Go to the command prompt, and install nedb.

npm install nedb

Create the file src/scripts/Gateway.ts and paste the following lines:

import * as Nedb from 'nedb';

export interface IModifier {
    $set: any;
    $unset: any;
    $inc: any;
    $min: any;
    $max: any;
    $push: any;
    $pop: any;
    $addToSet: any;
    $pull: any;
    $each: any;
    $slice: any;
}

export interface IQuery<T> {
    $lt: T;
    $lte: T;
    $gt: T;
    $gte: T;
    $in: T;
    $nin: T;
    $ne: T;
    $exists: T;
    $regex: RegExp;
}

export interface IDoc {
    _id: string;
}

export default class Gateway<T> {
    db: Nedb;

    constructor(file?: string) {
        this.db = new Nedb({
            filename: file,
            autoload: true
        });
    }

    create(doc: T): Promise<T & IDoc> {
        return new Promise((resolve, reject) => {
            this.db.insert(doc, function (err, newDoc) {
                if (err) {
                    reject(err);
                } else {
                    resolve(newDoc as any);
                }
            });
        });
    }

    get(_id: string): Promise<T & IDoc> {
        return new Promise((resolve, reject) => {
            this.db.findOne({ _id: _id }, function (err, doc) {
                if (err) {
                    reject(err);
                } else {
                    resolve(doc as any);
                }
            });
        });
    }

    update(_id: string, doc: T | IModifier): Promise<T & IDoc> {
        return new Promise((resolve, reject) => {
            this.db.update({ _id: _id }, doc, {}, function (err, docs) {
                if (err) {
                    reject(err);
                } else {
                    resolve(docs[0]);
                }
            });
        });
    }

    find(query: Partial<T> | keyof T): Promise<(T & IDoc)[]> {
        return new Promise((resolve, reject) => {
            this.db.find(query, function (err, docs) {
                if (err) {
                    reject(err);
                } else {
                    resolve(docs);
                }
            });
        })
    };

    delete(_id: string): Promise<number> {
        return new Promise((resolve, reject) => {
            this.db.remove({ _id: _id }, {}, function (err, numRemoved) {
                if (err) {
                    reject(err);
                } else {
                    resolve(numRemoved);
                }
            });
        });
    }
}

This will create a Gateway for accessing our database.

Now create a folder data in the root of your project. This will be used for storing our database. If you need to reset any of the data, simply delete the files in this directory. Also, if you decide to store your project in source control, do not commit this directory.

Service

Now that we have a database, let's create our service to access the data.

We are going to create a list of Products that a user can manage. They will have a name and a description.

Create a file src/scripts/data/IProduct.ts, and paste the following lines:

export interface IProduct {
    name: string;
    description: string;
}

This interface will define the fields of our Product.

Now create a file src/scripts/controllers/ProductController.ts, and paste the following lines:

import { Controller, method } from 'sierra';

import Gateway from './Gateway';

import { IProduct } from '../data/IProduct';

export default class ProductController extends Controller {
    productGateway: Gateway<IProduct> = new Gateway<Test>('data/products.db');
}

Note that we have created a Controller called ProductController. We also imported both IProduct and Gateway to create a gateway property. Our Gateway instance points at a file data/products.db which will store our data.

Also note that we didn't specify a base route. Sierra will then generate a route automatically based on the Controller's name. In this case, the base will be set to product.

Now let's create our Controller methods. We are creating a RESTful API, which in general conform to certain standards. We want to support five operations: List, which will give us all records, and our "CRUD" operations: Create, Read, Update, Delete.

  1. List - Uses the GET HTTP method, and is the default route /product.
  2. Create - Uses the POST HTTP method, and is the default route /product.
  3. Read - Uses the GET HTTP method, and is the route with an id parameter, /product/:id.
  4. Update - Uses the PUT HTTP method, and is the route with an id parameter, /product/:id.
  5. Delete - Uses the DELETE HTTP method, and is the route with an id parameter, /product/:id.

Let's create the List method. Inside the class, add the following lines:

@method('get')
async index() {
    return await this.gateway.find({});
}

Here we have created another index method, but in this case we are returning the results of this.gateway.find({}), which will return all rows of our products.

Now let's create the Create method. Inside the class, add the following lines:

@method('post')
async post($body: IProduct) {
    return this.gateway.create($body);
}

Here we again used another reserved method name, post. If we use a reserved method name that matches the HTTP method, we will use the default route. Then we used a "special parameter" $body, which is provided by Sierra to represent the Post Body data. We then store the $body with this.gateway.create($body).

For our next three methods, we are going to use "query parameters". These are named portions of the URL pathname which are parsed and passed as parameters.

@method('get', '/:id')
async get(id: string) {
    return await this.gateway.get(id);
}

Here we defined our route as /:id, which is appended onto the product base route. It is then automatically passed as the id parameter. These parameters are all type string. We then pass the id to this.gateway.get(id).

For our remaining methods, we use similar ideas.

@method('put', '/:id')
async put(id: string, $body: IProduct) {
    return this.gateway.update(id, $body);
}

@method('delete', '/:id')
async delete(id: string) {
    return this.gateway.delete(id);
}

Testing

In order to test our service, we will need a better tool than a browser. Download Postman from https://www.getpostman.com/.