A single language for backend and frontend from AngularJS to cloud with Claudia.js

58
A single language for Backend and Frontend: from AngularJS to Cloud AngularConf 2016 - Turin

Transcript of A single language for backend and frontend from AngularJS to cloud with Claudia.js

A single language for Backend andFrontend: from AngularJS to Cloud

AngularConf 2016 - Turin

corley.it

Walter Dal Mut

github.com/wdalmut

twitter.com/walterdalmut

the problem

The REST webserviceDecouplingCachingStatelessQuota planning (API Limits)ExtendableProxableetc...

PlanningGET → /book Access to a list of books

GET → /book/:id Access to a single book

POST → /book Create a new book

PATCH → /book/:id Update a given book

DELETE → /book/:id Delete a given book

We can extends this using authentication header Authorization

We have a simple API'use strict' ;

angular.module( 'myApp').service( 'bookService' , function($http) return "list": function() return $http.get( "http://localhost:8085/book" ); , ... // more methods ; );

So we create a serviceOr we can create a provider to configure the URLs etc

With a service we have a boxwith different methods

bookServiceGet all books: list()Get a single book: get(:bookId)Create a book: create(:bookModel)...

The Book model is stable across our applicationtitle: "the book title" , isbn: "12345678" , available: true

Or we resolve the book dependency with the book link (thanks to API uniquiness)

name: "my favourites" , books: [ 1,2,3]

So:$q.all( [1,2,3].map(bookService.get) ).then(function(books) //books is my favourites list with details );

Where the GET'use strict' ;

angular.module( 'myApp').service( 'bookService' , function($http) return "get": function(id) return $http.get( "http://localhost:8085/book/" +id); , "list": function() return $http.get( "http://localhost:8085/book" ); , ... // more methods ; );

Return back to

A controller can request dataangular.module( 'myApp') .controller( 'MainCtrl' , function ($scope, bookService) $scope.books = []; bookService.list().success(function(books) $scope.books = books; ); );

Thanks to the controller we can pass our books to the view

The scope the communication channel for the data

Q: how can i verify the scope interface?

R: via testing (unit testing)?

Add a test case (1/2)describe("MainCtrl" , function() beforeEach(module( 'myApp'));

beforeEach(inject(function ($controller, $rootScope, $httpBackend) scope = $rootScope.$new(); httpBackend = $httpBackend; MainCtrl = $controller( 'MainCtrl' , $scope: scope, // other mocks ); ));

// continue... );

Add a test case (2/2)it('should attach a list of books to the scope' , function () // when someone require the "/book" endpoint reply with a valid response httpBackend.whenGET( "http://localhost:8085/book" ).respond([ id: 1, title: "This is a book" , isbn: "9835623", available: true , id: 2, title: "Another super book" , isbn: "9835624", available: true , id: 3, title: "A rare book to read" , isbn: "9835625", available: false ]); // force the HTTP data resolution httpBackend.flush();

// my expectation are that we books in the controller scope. expect(scope.books.length).toBe( 3); expect(scope.books.map((item) => item.title)).toEqual([ "This is a book" , "Another super book" , "A rare book to read" ]); );

$httpBackend act as a spy for HTTP requests

main.html<!­­ books is in the current scope ­­> <books­list books="books"></books­list >

The directive (web component)angular.module("myApp") .directive( 'booksList' , function() return replace: true, templateUrl: "app/views/books­list.html" , restrict: "E", scope: "books": "=", , link: function() , ; );

books-list.html<div> <book ng­if="book.available" book="book" ng­repeat="book in books" ></book> </div>

The book web componentangular.module("myApp") .directive( 'book', function() return replace: true, templateUrl: "app/views/book.html" , restrict: "E", scope: "book": "=", , link: function() , ; );

book.html<div> book.title </div>

Components testing (1/3)describe("Books List" , function() var $compile, $rootScope;

beforeEach(module( 'myApp'));

beforeEach(function() inject(function( _$compile_, _$rootScope _) $compile = _$compile_; $rootScope = _$rootScope _; ); ););

Components testing (2/3)it("should expose our books" , function() $rootScope.books = [ id: 1, title: "This is a book" , isbn: "9835623", available: true id: 2, title: "Another book" , isbn: "9835624", available: true id: 2, title: "A secret book" , isbn: "9835625", available: false ]; var element = $compile( '<books­list books="books"></books­list>' )($rootScope);

$rootScope.$digest();

var html = element.html(); expect(html).toContain( "This is a book" ); expect(html).toContain( "Another book" ); expect(html).not.toContain( "A secret book" ); );

Components testing (3/3)Single book directive testing

it("should expose a single book" , function() $rootScope.book = id: 1, title: "This is a single book" , isbn: "9835623", available: true ; var element = $compile( '<book book="book"></book>' )($rootScope);

$rootScope.$digest();

var html = element.html(); expect(html).toContain( "This is a single book" ); );

Add a book creationWe need the create book service methodWe need another dummy web componentWe use the controller to save the book model

The create methodangular.module( 'myApp').service( 'bookService' , function($http) return "create": function(book) return $http.post( "http://localhost:8085/book" , book); , /* already implemented */ "get": function(id) return $http.get( "http://localhost:8085/book/" +id); , "list": function() return $http.get( "http://localhost:8085/book" ); , ; );

Dummy web component (1/3)angular.module("myApp") .directive( 'bookForm' , function() return replace: true, templateUrl: "app/views/book­form.html" , restrict: "E", scope: "book": "=", "save": "=", ; );

We can also skip the book model pass-through

Dummy web component (2/3)<div> <form> Title: < input type="text" ng­model= "book.title" /><br> ISBN: < input type="text" ng­model= "book.isbn" /><br> Available: < input type="checkbox" ng­model= "book.available" /><br> <button type="button" ng­click= "save(book)" >Save</button>< br> </form> </div>

Pay attention on ng-click that pass the book model

Dummy web component (3/3)it("should expose a book", function(done)

$rootScope.book = ;

$rootScope.save = function(book)

expect(book.title).toEqual("Some text");

expect(book.isbn).toEqual("123456");

expect(book.available).toBe(true);

done();

;

var element = $compile('<book­form book="book" save="save"></book­form>')($rootScope);

$rootScope.$digest();

angular.element(element.find('input')[0]).val('Some text').triggerHandler('input');

angular.element(element.find('input')[1]).val('123456').triggerHandler('input');

angular.element(element.find('input')[2]).prop('checked', 'checked').triggerHandler('click');

$rootScope.$apply();

element.find('button')[0].click();

);

So we have validated that the directive calls the save method

But... Who have the save method? Controller + Book Service

Previous Controller (1/2)angular.module( 'myApp') .controller( 'MainCtrl' , function ($scope, bookService) $scope.book = ; $scope.save = function(book) bookService.create(book).success(function(book) $scope.books = $scope.books.concat([book]); ); $scope.book = ; ;

// old parts... $scope.books = []; bookService.list().success(function(books) $scope.books = books; ); );

<book­form book="book" save="save"></book­form>

Previous Controller (2/2)it('should create a new book', function () // something uses the save method var book = title: "This is a book", isbn: "9835623", available: true ; scope.save(book);

// then the backend should works as expected httpBackend.whenGET("http://localhost:8085/book").respond([]); httpBackend.whenPOST("http://localhost:8085/book").respond( Object.assign(, book, id: 1) ); httpBackend.flush();

expect(scope.book).toEqual(); expect(scope.books.length).toBe(1); expect(scope.books[0].id).toBe(1); expect(scope.books[0].title).toEqual("This is a book"); expect(scope.books[0].isbn).toEqual("9835623"); expect(scope.books[0].available).toBe(true); );

We can develop the app but...

How to deliver a scalable andcost-effective backend?

ServerLess environmentsThose envs allows us to pay just for every single requests

No requests? No payments! [more or less]

No server maintenance (platform as a service)

Amazon Web ServicesAWS API Gateway []AWS Lambda []AWS DynamoDB []AWS SNS [ ]AWS SQS [ ]AWS CloudWatch [ ]so many services... [ ... ]

API Gateway

Thanks to API Gateway you can create/publish/maintain/monitorand secure APIs at any scale

Essentially: with API Gateway you declare your APIs interfaces and delegate to another component (like Lambda,EC2, etc) the functionality

Lambda is a compute service that can run the code on your behalfusing AWS infrastructure

Amazon DynamoDB is a fully managed NoSQL database service thatprovides fast and predictable performance with seamless scalability.

["apiGateway", "lambda", "dynamodb"].reduce(myAppFn);

Dealing with ApiGateway + Lambda

manually is tricky

AWS releases a project: ServerLess thathelps a lot the wiring

https://github.com/serverless/serverless

ServerLess is interesting but for REST APIis not super simple to use imho...

I prefer to use Claudia.jshttps://github.com/claudiajs/claudia

Claudia expose a very simple interfacevar ApiBuilder = require('claudia­api­builder' ), api = new ApiBuilder();

api.get('/hello', function (request, response) return "Hello"; );

Similar to express or hapi

You can use return codes and morefeatures...

api.post('/account' , function (request) return api.ApiResponse ( name: "Walter", surname: "Dal Mut" , 201); );

Claudia.js prepare the whole ApiGateway and Lambdaconfiguration (with CORS) automatically for you

claudia create \ ­­name book­ module \ ­­region eu­west­ 1 \ ­­api­module book \ ­­config claudia­books.json

It creates a new module book-module and store in claudia-books.json the Claudia.js conguration

Claudia.js manage also all updatesclaudia update \ ­­config claudia­ conf.json

So we have to develop our API

Thanks to Joi we can add data validation to our API

var joi = require(' Joi');var result = joi.object().keys( id: joi.number().required(), title: joi. string().min(1).required() ).validate(request.body);

if (result.error) return new api.ApiResponse (result.error, 406);

// continue

Claudia.js manage JSON data automatically

Claudia.js manage A+ Promises as areturn element

api.post("/say­ok", function(request) var d = q.promise();

setTimeout( function() d.resolve( "OK"); , 2000);

return d.promise; );

So...api.post("/say­ok", function(request) // validation...

return db.save(request.body); );

For Node.js DynamoDB offers a "Document client"

docClient = new AWS.DynamoDB.DocumentClient(region: "eu­west­1" );

docClient.put( Item: name: "Walter", surname: "Dal Mut", TableName: tableName ).promise();

docClient.get( ...); // primary key docClient.query( ...); // on a secondary index and/or primary key docClient.scan( ...); // search without index (table scan)

All methods have a A+ Promise implementation integrated

Put all togetherapi.post("/book", function(request) var d = q.promise();

var result = joi.object().keys( title: joi. string().min(1).required(), isbn: joi. string().min(1).required(), available: joi.boolean().required() ).validate(request.body);

if (result.error) return new api.ApiResponse (result.error, 406);

var id = uuid.v4(); var item = Object.assign(, id: id, request.body); docClient.put( Item: item, TableName: tableName).promise().then(function() d.resolve(item); );;

return d.promise; );

Of course you can split all responsabilities to dierent components

Thank you for listeningIf you are interested in workshops and/or training sessions feel free

to contact [email protected]

Check out also our page for training at: corsi.corley.it

Check out our corporate website: corley.it