# SPIKE: AngularDart REST Client Every client-side applications has to talk to REST APIs. At the moment AngularDart does not provide any high-level abstractions to help you do that. You can send http requests, but that's it. This post is about a spike I did a few days ago to explore possible ways of building such a library. It also shows that you can do quite a bit in just one hundred lines of Dart. ## Design Principles ### Plain old Dart objects. No active record. Angular is different from other client-side frameworks. It lets you use simple framework-agnostic objects for your components, controllers, formatters, etc. In my opinion making users inherit from some class is against the Angular spirit. This is especially true when talking about domain objects. They should not have to know anything about Angular or the backend. Any object, including a simple 'Map', should be possible to load and save, if you wish so. This means that: post.save() post.delete() are not allowed. ### Convention over Configuration Everything should work with the minimum amount of configuration, but, if needed, be extensible. It should be possible to configure how data is serialized, deserialized, etc. ## Now, let's look at some code... There are three main components: `Resource`, `ResourceStore`, and `ObjectStore`. ### Resource We can create a `Resource` object using the `res(type, id, [content])` function: res("posts", 1, {"title": "some post"}) ## ResourceStore We can create, update, and load resources from the server using `ResourceStore`. We can get one entity if we know its type and id. it("fetches a resource", inject((MockHttpBackend hb, ResourceStore store) { hb.when("/some/123").respond('{"id": 123, "field" : "value"}'); waitForHttp(store.one("some", 123), (Resource res) { expect(res.id).toEqual(123); expect(res.content["field"]).toEqual("value"); }); })); By default, the resource type is used to construct the url, but we can change it by configuring the store: beforeEach(inject((ResourceStore store) { store.config = { "some" : {"route": "secret"} }; })); it("uses the configuration", inject((MockHttpBackend hb, ResourceStore store) { hb.when("/secret/123").respond('{"id": 123}'); waitForHttp(store.one("some", 123), (Resource res) { expect(res.id).toEqual(123); }); })); ### One and List Suppose we have our store configured as follows: beforeEach(inject((ResourceStore store) { store.config = { "posts" : {"route": 'posts'}, "comments": {"route": "comments"} }; })); We can load one or many posts: it("returns a post", inject((MockHttpBackend hb, ResourceStore store) { hb.when("/posts/123").respond('{"id": 123, "title" : "SampleTitle"}'); waitForHttp(store.one("posts", 123), (Resource post) { expect(post.id).toEqual(123); expect(post.content["title"]).toEqual("SampleTitle"); }); })); it("returns many posts", inject((MockHttpBackend hb, ResourceStore store) { hb.when("/posts").respond('[{"id": 123, "title" : "SampleTitle"}]'); waitForHttp(store.list("posts"), (List posts) { expect(posts.length).toEqual(1); expect(posts[0].content["title"]).toEqual("SampleTitle"); }); })); ### Nested Resources We can also load nested resources: it("returns a comment", inject((MockHttpBackend hb, ResourceStore store) { hb.when("/posts/123/comments/456").respond('{"id": 456, "text" : "SampleComment"}'); waitForHttp(store.scope(res("posts", 123)).one("comments", 456), (Resource comment) { expect(comment.id).toEqual(456); expect(comment.content["text"]).toEqual("SampleComment"); }); })); To do that we had to scope our store using another resource: ResourceStore post123Store = store.scope(res("posts", 123)) We can do it as many times as we want: store.scope(res("blogs", 777)).scope(res("posts", 123)); ### Updates A resource can be saved as follows: it("saves a post", inject((MockHttpBackend hb, ResourceStore store) { hb.expectPUT("/posts/123", '{"id":123,"title":"New"}').respond(['OK']); final post = res("posts", 123, {"id": 123, "title": "New"}); waitForHttp(store.save(post)); })); And, as you have probably guessed, saving a comment involves scoping: it("saves a comment", inject((MockHttpBackend hb, ResourceStore store) { hb.expectPUT("/posts/123/comments/456", '{"id":456,"text":"New"}').respond(['OK']); final post = res("posts", 123); final comment = res("comments", 456, {"id": 456, "text" : "New"}); waitForHttp(store.scope(post).save(comment)); })); ## ObjectStore That's all well and good, but a bit too low-level. That is why there are other abstractions that are built on top of `ResourceStore`. One of them is `ObjectStore`. Suppose we have these classed defined: class Post { int id; String title; } class Comment { int id; String text; } We want to be able to work with `Post`s and `Comment`s, not with `Map`s. To do that we need to configure our store: beforeEach(inject((ResourceStore store) { store.config = { Post : { "route": 'posts', "deserialize" : deserializePost, "serialize" : serializePost }, Comment : { "route": "comments", "deserialize" : deserializeComment, "serialize" : serializeComment } }; })); Where the serialization and deserialization functions are responsible for converting domain objects from/into resources. Post deserializePost(Resource r) => new Post() ..id = r.id ..title = r.content["title"]; Resource serializePost(Post post) => res(Post, post.id, {"id" : post.id, "title" : post.title}); Comment deserializeComment(Resource r) => new Comment() ..id = r.id ..text = r.content["text"]; Resource serializeComment(Comment comment) => res(Comment, comment.id, {"id" : comment.id, "text" : comment.text}); ### Using Post and Comment Having this config in place, we can load and save `Post`s and `Comment`s. We do not have to work with `Resource` at all. it("returns a post", inject((MockHttpBackend hb, ObjectStore store) { hb.when("/posts/123").respond('{"id": 123, "title" : "SampleTitle"}'); waitForHttp(store.one(Post, 123), (Post post) { expect(post.title).toEqual("SampleTitle"); }); })); it("returns many posts", inject((MockHttpBackend hb, ObjectStore store) { hb.when("/posts").respond('[{"id": 123, "title" : "SampleTitle"}]'); waitForHttp(store.list(Post), (List posts) { expect(posts.length).toEqual(1); expect(posts[0].title).toEqual("SampleTitle"); }); })); it("returns a comment", inject((MockHttpBackend hb, ObjectStore store) { final post = new Post()..id = 123; hb.when("/posts/123/comments/456").respond('{"id": 123, "text" : "SampleComment"}'); waitForHttp(store.scope(post).one(Comment, 456), (Comment comment) { expect(comment.text).toEqual("SampleComment"); }); })); ### Updates it("saves a post", inject((MockHttpBackend hb, ObjectStore store) { hb.expectPUT("/posts/123", '{"id":123,"title":"New"}').respond(['OK']); final post = new Post()..id = 123..title = "New"; waitForHttp(store.save(post)); })); it("saves a comment", inject((MockHttpBackend hb, ObjectStore store) { hb.expectPUT("/posts/123/comments/456", '{"id":456,"text":"New"}').respond(['OK']); final post = new Post()..id = 123; final comment = new Comment()..id = 456..text = "New"; waitForHttp(store.scope(post).save(comment)); })); ### Serializers and Deserializers We do not have to define custom serializers and use reflection instead. beforeEach(inject((ResourceStore store) { store.config = { Post : { "route": 'posts', "deserialize" : new MirrorBasedDeserializer(["id", "title"]), "serialize" : new MirrorBasedSerializer(["id", "title"]) } }; })); it("returns a post", inject((MockHttpBackend hb, ObjectStore store) { hb.when("/posts/123").respond('{"id": 123, "title" : "SampleTitle"}'); waitForHttp(store.one(Post, 123), (Post post) { expect(post.title).toEqual("SampleTitle"); }); })); it("saves a post", inject((MockHttpBackend hb, ObjectStore store) { hb.expectPUT("/posts/123", '{"id":123,"title":"New"}').respond(['OK']); final post = new Post()..id = 123..title = "New"; waitForHttp(store.save(post)); })); ## SimpleObjectStore When given an object, `ObjectStore` determines its resource type based on its runtime type. That is OK for the situations when there is one-to-one correspondence between domain objects and resources. It is not always the case, however. If you need more flexibility use `SimpleObjectStore`. it("returns a comment", inject((MockHttpBackend hb, SimpleObjectStore store) { hb.when("/posts/123/comments/456").respond('{"id":123, "text" : "SampleComment"}'); waitForHttp(store.scope('posts', 123).one("coment", 456), (Map comment) { expect(comment["text"]).toEqual("SampleComment"); }); })); it("saves a comment", inject((MockHttpBackend hb, SimpleObjectStore store) { hb.expectPUT("/posts/123/comments/456", '{"id":456,"text":"New"}').respond(['OK']); final comment = {"id" : 456, "text": 'New'}; waitForHttp(store.scope('posts', 123).save("comments", 456, comment)); })); ## Just a Spike This is just a spike. So it covers only a small subset of the features you would expect from such a library. ### Query API The only queries that are supported right now are "find by id" and "find all". Obviously, that is not enough. ### Associations Modelling associations is a big one. One way to do that is be able to declare them when configuring the store. beforeEach(inject((ResourceStore store) { store.config = { Post : { "route": 'posts', "hasMany": {"comments" : Comment}, "deserialize" : deserializePost, "serialize" : serializePost }, Comment : { "route": "comments", "deserialize" : deserializeComment, "serialize" : serializeComment } }; })); Then, you should be able to include/exclude them when fetching/saving objects: Future postWithComments = store.one(Post, 123); Future postWithoutComments = store.one(Post, 123, exclude: ['comments']); Modelling associations is extremely difficult, and, as a result, may be pushed back till later. ### Identity Map Implement an identity map. ### jsonapi.org Experiment with conforming to the jsonapi.org format. ### Dirty Checking It would be interesting to look into ways of using the Angular dirty checking to determine what should be sent to the server. ### Serializers Such things as `MirrorBasedSerializer` should be extracted into a separate package (maybe there is an existing package that can be used). As Victor Berchet pointed out, the smoke package can be used there. ## Source Code + Tests It is just a spike, and it is about 100 lines of Dart code (+ tests). You can check it out [here](https://gist.github.com/vsavkin/e80d68d8bfc0b2074c37). ## Not a Spike... I'm going to build a production version of this library soon. If you have any comments or suggestions, please, message me on twitter @victorsavkin or leave a comment down below.