Pattern for smooth SPA development with SWI-Prolog

The gist of this pattern is to use SWI-Prolog to serve your application page. Then when your application requests JavaScript that it doesn’t have, it behaves like a proxy, passing the request onto the SPA development server and returning the response.

I use this with ClojureScript and Logtalk with SWI-Prolog on the backend, but there’s no reason it shouldn’t work with other SPA’s (I’ve also used VueJS) and without Logtalk. If Web-Development with SWI-Prolog is new to you, you might want to run through Annie’s fantastic tutorial first.

Step 1: Use SWI-Prolog to Serve the App Page

SPA applications work with an index.html page you’ll find somewhere in your SPA directory. In that index.html there will be a <div/> or some such tag where the application will appear, as well as some scripts that make the magic happen. You can either copy this file across to your SWI-Prolog directory and have SWI-Prolog serve it, or recreate it with termerized HTML.

Usually I opt for termerized HTML so I can use it for consistency across webpages, such as having the same navbar. I also use termerized HTML so I can get dynamic behaviour, such as only rendering a “Logout” button if a user is logged in. So to achieve Step 1 here for ClojureScript, I’d have something like this:

:- handler('/myapp/', app_page, [id(myapp)]).

app_page(_Request) :-
	reply_html_page(
		[title(myapp)],
		[
			div(id='app', [p('Loading ...')]),
			script([src='js/compiled/myapp.js', type='text/javascript'], []),
			script(type='text/javascript', 'myapp.core._main();')
		]).

Step 2: Getting that JavaScript

For my ClojureScript application to run, I’m requesting some JavaScript at js/compiled/myapp.js. In turn, this triggers requests for a too-many-to-bother-counting number of JavaScript files. None of these files are visible to my SWI-Prolog application. Also, none of these files are used in production: by then they’re all compiled down to one “little” file.

So what I want SWI-Prolog to do when it’s asked for these files is to ask whatever is serving my SPA for them. This means I need whatever process is used to typically serve my SPA also running during development, for ClojureScript that’s lein figwheel. That serves on http://localhost:3449, you’ll need to adapt the code below for your SPA.

I get SWI-Prolog to serves these files by combining the prefix we use for serving static files with SWI-Prolog’s HTTP Client libraries. In my app the requests to the scripts are relative to the URL being served, so it’s actually going to ask for /myapp/js/compiled/myapp.js, which is why the handler here is root(myapp). If yours is an absolute URL, adapt accordingly.

:- use_module(library(http_client), [ http_get/3 ]).
:- use_module(library(http_open), [ http_close_keep_alive/1 ]).
:- use_module(library(http_header), [ http_reply_header/3 ]).

:- handler(root(myapp), spa_proxy, [id(spa_proxy), prefix, priority(-1)]).

handle(Request) :-
	proxy_url(Request, Url),
	proxy(Url).

proxy_url(Request, Url) :-
	member(path_info(RelPath), Request) ; RelPath = '/'), !,
	% comment these writes and nl out when you've seen it working!
	write(user_output, 'Forwarding: '),
	write(user_output, RelPath),
	nl(user_output),
	% adapt the base url to your spa
	atom_concat('http://localhost:3449', RelPath, URL).

proxy(Url) :-
	http_get(Url, Data, [
		headers(Headers),
		to(string),
		connection('Keep-alive')]),
	http_close_keep_alive(URL),
	member(content_type(ContentType), Headers),
	write('Content-type: '), write(ContentType),
	nl,
	nl,
	write(Data).

Step 3: Enjoy!

That should be about all you need. Spin up both processes, navigate to your SPA app served by SWI-Prolog, and watch the magic happen. When it comes to production deployment just change the myapp handler to return HTML that loads your compiled JavaScript instead of the development JavaScript, and don’t load the spa_proxy handler. I do this with Logtalk, so for me this is just commenting out the a file in the loader.lgt, but you can do the same with SWI-Prolog if you also use some kind of loader file.

This was a bit of a pain to figure out the first time, but has saved me so much bother and made developing frontend applications on a Prolog backend quite practical. I hope it helps you out too!