Easy route management for Java Spark applications

Even a small application can have many URL routes, which grow difficult to manage over time.

Frameworks like Rails and Django provide a lot of functionality to easily express complex routes and map them to internal actions. However in frameworks like Spark Java it tends to require a lot more code to acheive the same thing.

Goals

There’s two goals I try to adhere to whenever managing my routes –

  1. Readability – organizing them into smaller more paresable sections
  2. Easy versioning – Building new routes to serve content while simultaneously supporting old route mappings. Generally this is much more applicable to JSON API’s than HTML content, but it could be either.

To address readability I strive to keep each resource’s (e.g. User, Post, etc..) routes in a separate file.

To enable easy versioning I run multiple instances of the Spark server.

Running multiple Spark instances is only available in version 2.5 (May 2016) or later

A flexible design pattern

We create an HttpContext for each version we want to support. Each context is essentially it’s own Spark application, so we could host /api/v1 on one instance and /api/v2 on another.

                
// HttpContext.java
package me.abhchand.example;

import com.google.gson.Gson;

import spark.Service;
import spark.Spark;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HttpContext {

  private static final Logger LOG = LoggerFactory.getLogger(HttpContext.class);

  private final Service http;

  private final String basePath;

  public HttpContext(String basePath, int port) {
    this.basePath = basePath;
    this.http = Service.ignite().port(port);

    this.registerExceptionHandler();
  }

  public void registerRoutes(RouteBuilder builder) {
    builder.register(http, basePath);

    LOG.info("REST routes registered for {}.", builder.getClass().getSimpleName());
  }

  public void enableCors() {
    http.before((req, res) -> {
      res.header("Access-Control-Allow-Origin", "*");
      res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
      res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
    });

    LOG.info("CORS support enabled.");
  }

  private void registerExceptionHandler() {
    // User data error throws `IllegalArgumentException`, in which case
    // return a 400 code since the request was bad
    http.exception(IllegalArgumentException.class, (e, req, res) -> {
      res.status(400);
      res.body((new Gson()).toJson(e.getMessage()));
    });

    // For all other internal errors, return a 500
    http.exception(Exception.class, (e, req, res) -> {
      res.status(500);
      res.body("{\"body\": \"An error has occurred\" }");
    });
  }
}
                
              

Secondly we define a RouteBuilder interface that lets us define routes for individual resources. Below we create routes for the User resource.

                
// RouteBuilder.java

package me.abhchand.example;

import spark.Service;

public interface RouteBuilder {
  void register(Service http, String basePath);
}
                
              
                
// UserRoutes.java

package me.abhchand.example;

import spark.Service;

public class UserRoutes implements RouteBuilder {

  @Override
  public void register(Service http, String basePath) {
    http.get(    basePath + "/users",     new UsersIndexHandler(basePath));
    http.get(    basePath + "/users/:id", new UsersShowHandler(basePath));
    http.post(   basePath + "/users",     new UsersCreateHandler(basePath));
    http.put(    basePath + "/users/:id", new UsersUpdateHandler(basePath));
    http.delete( basePath + "/users/:id", new UsersDestroyHandler(basePath));
  }
}
                
              

The individual handlers seen above aren’t part of this example, but they are simply objects that implement the spark.Route interface and return a String response.

Some implementations like to pass each handler a model object to generate the response. In most of my implementations I have the handler itself query the database for the model object. Either is fine, it should just be consistent and tailored to your implementation’s needs

Now that the piping is in place, it’s simply a matter of creating as many HTTP contexts as needed

                
// ApplicationMain.java

package me.abhchand.example;

import spark.Spark;

public class ApplicationMain {

  public static void main(String[] args) {
    //
    // Initialize a new Spark instance for /api/v1 and register the routes
    //
    HttpContext apiV1 = new HttpContext("/api/v1", 3000);

    apiV1.enableCors();
    apiV1.registerRoutes(new UserRoutes());


    //
    // Initialize a new Spark instance for /api/v2 and register the routes
    //
    HttpContext apiV2 = new HttpContext("/api/v2", 3001);

    apiV2.enableCors();
    apiV2.registerRoutes(new UserRoutes());
  }
}
                
              

Here we’ve used the same route mappings for /api/v1 and /api/v2, but more practically you could set up additional route builder classes and pass each version a different route mapping as needed.