January 31, 2016

Java EE with JAX-RS + JPA vs. Node.js with Hapi + Bookshelf.js (part 2 of 2)


Pages: 1 2

HTTP endpoint definition

Here, the Java EE-based solution profits for the small abstraction layer introduced with Crudlet which comes with prepared HTTP endpoint definitions for the main CRUD operations according to de-facto REST standards. All you need to do is implement a CrudResource class with an almost empty body:
1
2
3
4
5
6
7
8
9
10
11
@Path("customers")
@Stateless
public class CustomerResource extends CrudResource<Customer> {
    @Inject
    private CustomerService service; // from "DB access definition" chapter
 
    @Override
    protected CrudService<Customer> getService() {
        return service;
    }
}
For custom HTTP endpoints or more sophisticated declarations (such as the payments nested base resource), use the simplistic JAX-RS annotations, or manipulate the control flow programmatically:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Path("customers/{customerId}/payments")
@Stateless
public class PaymentResource extends CrudResource<Payment> {
    @Inject
    private PaymentService service;
    @Inject
    CustomerService customerService;
     
    @Override
    protected CrudService<Payment> getService() {
        return service;
    }
 
    @Override
    public List<Payment> findAll() {
        Long customerId = getPathParam("customerId", Long.class);
        return service.findAllByCustomer(customerId);
    }
     
    @Override
    public Response save(Payment entity) {
        Long customerId = getPathParam("customerId", Long.class);
        Customer customer = customerService.findById(customerId);
        entity.setCustomer(customer);
        return super.save(entity);
    }
}
A Hapi server doesn’t know the notion of a base resource to group endpoints; rather, you define several independent routes on the server:
1
2
3
4
5
6
7
8
9
10
11
12
// find all
server.route({
    method: 'GET',
    path: config.basePath,
    handler: function (request, reply) {
        doQuery(...).then(function (collection) {
            reply(collection);
        });
    }
});
 
...
The Hapi API is quite elegant; however, for the ever-same CRUD operations on every single model, the server config may get quite bloated. For a fair comparison, I’ve locally built a similar abstraction layer for the main CRUD operations as I have done with Crudlet for the Java EE part in the hapiCrud.js script.

Now we can configure the Hapi server for CRUD on each REST base path like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
hapiCrud(server, {
basePath: '/customers',
    bookshelfModel: Customer,
    beforeAdd: function(request) {
        delete request.payload.payments;
    },
    beforeUpdate: function(request) {
        delete request.payload.payments;
    }
});
 
hapiCrud(server, {
basePath: '/customers/{customerId}/payments',
    baseQuery: function(request) {
        return {customer_id: request.params.customerId}
    },
    bookshelfModel: Payment,
    beforeAdd: function(request) {
        request.payload.customerId = request.params.customerId
    },
});
The REST server config also includes a generic solution for error / validation error handling, as addressed presently.

Error handling

For this demo application, it’s a design goal to return localization-ready error messages in case of a DB problem. For instance, deleting a customer with a non-empty payments list should return a 404 error with a body like this:
1
2
3
4
5
6
7
{
    "error": {
        "detailMessage": "DELETE on table 'CUSTOMER' caused a violation of foreign key constraint
            'PAYMENTCUSTOMER_ID' for key (1).  The statement has been rolled back.",
        "exception": "java.sql.SQLIntegrityConstraintViolationException"
    }
}
For the Java EE implementation, this kind of error handling is implemented in Crudlet. It returns the error message printed above.

For the Node.js server, we need to catch Bookshelf’s errors and let Hapi answer with a 404 message:
1
2
3
4
5
6
7
8
9
10
11
12
// delete
server.route({
    method: 'DELETE',
    path: config.basePath + '/{id}',
    handler: function (request, reply) {
        doQuery(...).then(function (entity) {
            reply().code(204);
        }).catch(function (err) {
            reply.response({error: {exception: err.code, detailMessage: err.message}}).code(400);
        });
    }
});
Because we explicitly return the raw error codes rather than localized error messages (because we want localization to happen on the client), and because Java’s and Bookshelf’s error codes do of course diverge, we need to maintain a separate set of I18N keys, for instance in an AngularJS client:

For a Java EE server:
1
2
'error.com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException':
    'Cannot delete an object which is still referenced to by other objects.',
For a Node.js server:
1
2
'error.ER_ROW_IS_REFERENCED_2':
    'Cannot delete an object which is still referenced to by other objects.',
The important part is that we can keep the localization logic the same.

Validation error handling

For this demo application, we also want to return localization-ready error messages in case of a model validation constraint violation. For instance, trying to save a customer with illegal characters in its name should return a 404 error with a body like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "validationErrors": {
        "name": {
            "attributes": {
                "flags": "[Ljavax.validation.constraints.Pattern$Flag;@1f414540",
                "regexp": "[A-Za-z ]*"
            },
            "constraintClassName": "javax.validation.constraints.Pattern",
            "invalidValue": "Name not allowed!!",
            "messageTemplate": "javax.validation.constraints.Pattern.message"
        }
    }
}
This again is implemented in Crudlet. The ConstraintViolationException thrown by Bean Validation contains all information about a constraint violation which gets handled accordingly by Crudlet.

Inside the Hapi routes, we can also catch Checkit’s errors and return the respective answer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// update
server.route({
    method: ['PUT', 'POST'],
    path: config.basePath + '/{id}',
    handler: function (request, reply) {
        if (typeof config.beforeUpdate !== 'undefined')
            config.beforeUpdate(request);
        doQuery(...).then(function (entity) {
            reply(entity);
        }).catch(config.bookshelfModel.ValidationError, function (err) {
            reply.response(transformConstraintViolationMessages(err)).code(400);
        });
    }
});
However, as mentioned before, Checkit doesn’t seem to provide as much information about a constraint violation as its Java Bean Validation counterpart does.

Again, we need sparate I18N keys to localize the validation error messages coming from a Java EE or a Node.js server, respectively. Here, it becomes obvious that the Node.js / Checkit solution just doesn’t provide as much information as Java Bean Validation does:

For a Java EE server:
1
'error.javax.validation.constraints.Pattern.message': 'must match "{{regexp}}"',
For a Node.js server:
1
'error.pattern': 'must match the expected pattern',

DB access definition

Note that we still have skipped the actual DB queries. This is also arguably the most boring work of the implementation, because they’re just basically the same CRUD queries for every REST endpoint.

Therefore, these operations are implemented in Crudlet already. Just implement CrudService:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CustomerService extends CrudService<Customer> {
    @Override
    @PersistenceContext
    protected void setEm(EntityManager em) {
        super.setEm(em);
    }
 
    @Override
    public Customer create() {
        return new Customer();
    }
 
    @Override
    public Class<Customer> getModelClass() {
        return Customer.class;
    }
}
Of course, you may add additional custom actions to the service where you’ll implement DB access using the EntityManager. This Java EE facility provides declarative and implicit transaction control.

DB access is what Bookshelf’s abstraction layer is made for. Hence, we use a Bookshelf model’s query operations to access the underlying DB (via Knex).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// update
server.route({
    method: ['PUT', 'POST'],
    path: config.basePath + '/{id}',
    handler: function (request, reply) {
        if (typeof config.beforeUpdate !== 'undefined')
            config.beforeUpdate(request);
        config.bookshelfModel.forge(request.payload).save().then(function (entity) {
            reply(entity);
        }).catch(config.bookshelfModel.ValidationError, function (err) {
            reply.response(transformConstraintViolationMessages(err)).code(400);
        });
    }
});
Bookshelf offers a clean, promise-based API which works excellently with Hapi.

Conclusion

I stated at the beginning that my opinion would be biased in favor of Java EE, and it clearly is. Both the Java EE and Node.js tech stack undoubtedly have their strengths and their flaws, but most importantly, I don’t see a single reason which would really let me turn down Java EE just now to write REST APIs. Both technologies offer a range of frameworks, and the opportunity to build your own, to make work easier. Most specifically, the important aspect of model validation currently seems superior in Java EE.

Some of the Node.js packages I covered here clearly are still in a quite early stage. This is especially true for Bookshelf and Knex. The documentation of Bookshelf and Knex in particular makes it frankly quite hard for new developers. It’s really just the API that is there, but no information about how to solve everyday real world  problems such as Bookshelf / Knex interplay, accessing / modifying relations, and how to properly include its own validation framework.

From a project management point of view, the idea of having the client and server written in the same language (isomorphic JavaScript) truly is compelling. The possibilities of a more rapid prototyping-like / “Ruby on Rails” like feeling as enabled by JavaScript’s dynamic typing are interesting as well. But again, in total it’s not strong enough an argument to turn down Java EE.

Also from a management position, perhaps my biggest reservation on Node.js is the lack of “official” standardization. The Java EE ecosystem really is built upon officially recognized software specifications which act as a contract for software vendors. We can learn and talk about specifications, and work with the implementation of our choice. In the Node.js world, there are hardly any globally recognized specifications (except e.g. for promises). We learn to use and talk about products: Node.js, Hapi, Bookshelf, these are all just product or current de-facto standards at most. They hardy offer investment protection; not if talking about 10+ years. The Java EE standards will still be around by then, and will probably further evolve, but no-one guarantees the survival of a single Node.js based product.

I’m still quite fascinated by the Node.js ecosystem and I will continue to study it for my own interest; but for true enterprise-quality software development, in my opinion, it is currently no match for a Java EE 7 tech stack, at least not in the use case discussed here.

Please let me know whether you found this article interesting and helpful, whether you agree with my conclusions or if you spotted an error of any kind.

Feel free to download the demo implementation of the Java EE 7 server or the Node.js server from their respective GitHub repositories.

Update February 9, 2016: Meanwhile, I have created the npm module hapi-bookshelf-crud, a simple abstraction layer on top of Hapi + Bookshelf.js + Joi to make CRUD REST endpoint creation as simple as possible. Head over to its npm package page to learn more.


Pages: 1 2

2 comments:

  1. Thank you for your relevant analysis. I also think nodejs is " too young " for future development in a company. Maybe nodejs is an interesting solution to the need for high performance ?

    ReplyDelete
    Replies
    1. Yes, I would conclude that for now, the Node.js ecosystem comes nowhere near to Java EE's maturity. I'm typically very cautious when it comes to global assertions about a technology's performance properties. However, as JAX-RS essentially compiles to plain Java servlets, I can hardly imagine Node.js / Hapi performing much better than that.

      Delete