Wrzasq.pl

FrontPageFilter - SPA with server side pages in Java

Thursday, 06 October 2016, 20:29

Time flies, browsers become more and more modern, they are more and more up-to-date. As the common web standards and new frontend technologies are being adopted you can shift more stuff to the client-side. Particularily it means nowadays you can leave most of the frontend part on your JavaScript application shoulders. Thanks to that SPA approach is getting more and more popular. Especially that most search engines and webmaster tools are now able to understand and execute JavaScript code to build full site representation for analysis. With all of these in mind it's an attractive idea to develop your backend application as a pure JSON API (it doesn't have to be pure REST) and handle everything related to presentation layer on a frontend (browser) side. But you still need an application to serve it all from your server(s). Which means your backend application still needs to be a web application.

Possible solutions

First of all, what are possible solutions to this problem?

  1. Keep site rendering logic on backend and handle frontend stuff with client-side JavaScript application. This is, obviously, the worst case - we keep a lot of stuff in both places (backend and frontend), backend needs all of the display logic dependencies, plus it doubles the work, as usually frontend also need to display same content.
  2. Node.JS application server in front of our backend that will use React server-side for the initial request. I don't like this approach - it adds Node.JS to the server stack, which is not needed for backend tasks (in my case - I'm a Java developer) and also, even though it reduces the workload, it keeps the rendering logic in both places (just what was Java/PHP/Python backend is now moved to Node.JS).
  3. Third option is to have catch-all route in the backend that will always render a bootstrap view of the web application and leave the action of the initial page to be taken by frontend application in the browser. This is closest to what I want to achieve, however there is one problem - every request to the website will end with 200 OK response.

Picked solution

Filtered request flow

Is there any other way? Yes there is! I created servlet filter that, when the request is done by the browser directly to the web application, forwards request to be processed as a JSON API call and wraps it in the view rendered to the response. Such implementation has several benefits:

  • It's transparent, decoupled from web application itself. Backend can be a plain JSON application, while still browsers accessing it will receive rendered web application.
  • The initial response is already there, bootstrapped client application won't have to execute AJAX call to fetch the response.
  • Filter propagates the response code produced by the backend, so accessing unexisting page will result in receiving 404 page in browser.
  • The solution is both backend and frontend technology-agnostic. You can use JSP, Facelets or Thymeleaf to render the bootstrap page; React, Angular or vanilla-JS on frontend.

The only condition that we need to keep in mind using this approach is that frontend routing will have to reflect backend API calls.

Note that it's just about any JSON API, not necesserali REST. Also, the very same approach would fit to other formats (XML for instance).

Implementation

Implementation of this concept is available in pl.wrzasq.commons:commons-web library. You can try it out (it's not yet in the Maven Central Repoistory, as it's still a development version).

Cases

Unfiltered request flow

The solution itself is quite simple, but the complex part here is the decision if the request/response should be filtered. Let me outline here the cases I considered:

  • First of all the request has to be GET HTTP method to indicate it's just a page load.
  • The request has to be issued with Accept header that indicates that it's a browser page load - either text/html or application/xhtml+xml content type must be accepted.
  • The response must produce any content - if this is empty response (eg. 202 Accepted) we don't want to add content to it - only wrap response if there is anythign to wrap (note that empty JSON stuff is still an existing output, eg null, {} or "" - empty string).
  • Error responses (with the precaution of previous points) should always return the rendered page.
  • If the response contains expicitely defined Content-Disposition header it means we explicitely defined to the client what to do with it, our filter shouldn't care (for example it's a file download).
  • Only wrap JSON responses (Content-Type header set to application/json).

Usage

To use the filter you will have to define it as a bean in your Spring application context - as the class is annotated, you can just add pl.wrzasq.commons.web to your auto-scanned packages:

@Configuration
@ComponentScan("pl.wrzasq.commons.web")
public class WebApplicationConfiguraiton
{
}

Afterwards, you need to define a filter in you web application descriptor (most likely web.xml) - we need to use DelegatingFilterProxy for that as our real filter will be instantiated by Spring on application bootstrapping:

<web-app …>
    …
    <filter>
        <!-- remember that filter name must match bean name -->
        <filter-name>wrzasqPlFrontPageFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>wrzasqPlFrontPageFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>ERROR</dispatcher>
    </filter-mapping>
</web-app>

Alternatively, if you are creating your web application context programatically, you can simply put your filter bean directly to the servlet definition (this example bases on Jetty):

servlet.addFilter(
    new FilterHolder(beanFactory.getBean(FrontPageFilter.class)),
    "/*",
    EnumSet.of(
        DispatcherType.REQUEST,
        DispatcherType.ERROR
    )
);

To instantiate the filter itself you need to have ViewResolver defined in your context and a property chillDev.frontPageFilter.viewName that is the view name for the template used to render the output.

Example

Here are some sample logs of HTTP requests and responses that shows the results and different behaviors (I skipped non-essential headers). Example of errored API call:

$ curl -v http://localhost:8181/nonexisting -H 'Accept: application/json'
> GET /nonexisting HTTP/1.1
> Accept: application/json
> 
< HTTP/1.1 404 Not Found
< Content-Type: application/json;charset=utf-8
{}

The same URL requested by browser would look like this:

$ curl -v http://localhost:8181/nonexisting
> GET /nonexisting HTTP/1.1
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< Content-Type: application/xhtml+xml;charset=utf-8
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>MyApp</title>
        <meta name="viewport" content="width=device-width, initial-scale=1"/>
    </head>
    <body>
        <!-- TODO: put it somewhere and use -->
        <script>
//<![CDATA[
var initialResponse = {
    status: 404,
    content: {}
};
//]]>
        </script>

        <main/>

        <footer/>

        <div id="fb-root"/>
    </body>
</html>

Sample proper request to non-API resource:

$ curl -v http://localhost:8181/robots.txt -H 'Accept: text/html'
> GET /robots.txt HTTP/1.1
> Accept: text/html
> 
< HTTP/1.1 200 OK
< Content-Type: text/plain;charset=utf-8
User-Agent: *
Disallow: 

Sample proper API request:

$ curl -v http://localhost:8181/blog -H 'Accept: application/json'
> GET /blog HTTP/1.1
> Accept: application/json
> 
< HTTP/1.1 200 OK
< Content-Type: application/json;charset=utf-8
{"sites":{"content":[]}}

And the same stuff rendered for the browser:

$ curl -v http://localhost:8181/blog
> GET /blog HTTP/1.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: application/xhtml+xml;charset=utf-8
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>MyApp</title>
        <meta name="viewport" content="width=device-width, initial-scale=1"/>
    </head>
    <body>
        <!-- TODO: put it somewhere and use -->
        <script>
//<![CDATA[
var initialResponse = {
    status: 200,
    content: {"sites":{"content":[]}}
};
//]]>
        </script>

        <main/>

        <footer/>

        <div id="fb-root"/>
    </body>
</html>

Spring dependency

Although the approach itself is generic, this particular implementation is dedicated for Spring MVC web applications - it depends on Spring-injected beans and also on Spring-specific view renderer. If you want to use this approach without Spring, you will have to make some adoptions.

Tags: , , , , , ,