JREST - Lightweight REST Library

This is a small project I created while making a Runescape bot. I needed a clean way to have multiple bots communicate with eachother, and I wanted it to work in a RESTful way. Spring Boot takes a year to turn on, so I needed something fast, and most other libraries require a million dependencies.

So here’s this fun little thing I whipped up in 2 days. It has fast startup time, and only requires Gson as a dependency.
Please don’t use this in production code… This is just a hobby project! Use something that’s been tried for much longer!

GITHUB:

Here’s some examples:

Synchronous Get:

RequestEntity<String> request = new RequestEntity<>(HttpMethod.GET);
ResponseEntity<String> response = request.exchange("http://localhost/testAPI", String.class);
System.out.println(response.getBody());

Asynchronous Get:

RequestEntity<JsonObject> request = new RequestEntity<>(HttpMethod.GET);
request.exchangeAsync("http://localhost/testJson", JsonObject.class, (response)->{
	System.out.println(response.getBody());
});

Simple REST Server:

public class TestServer extends RestServer {
	public TestServer() {
		
		/**
		 * Test Endpoint. Returns static String
		 */
		this.addEndpoint(HttpMethod.GET, "/testAPI", (request)->{
			return new ResponseEntity<String>(HttpStatus.OK, "Hello From Server!");
		});
		
		/**
		 * Test Post endpoint. Returns your posted data back to you.
		 */
		this.addEndpoint(HttpMethod.POST, "/testPost", MediaType.ALL, MediaType.ALL, (request)->{
			return new ResponseEntity<String>(HttpStatus.OK, request.getBody().toString());
		});
		
		/**
		 * Test JSON endpoint. Returns a JSON object.
		 */
		this.addEndpoint(HttpMethod.GET, "/testJson", MediaType.ALL, MediaType.APPLICATION_JSON, (request)->{
			JsonObject jsonObject = new JsonObject();
			jsonObject.addProperty("TestKey", "Hello World!");
			
			return new ResponseEntity<JsonObject>(HttpStatus.OK, jsonObject);
		});
	}
	
	@Override
	public int getPort() {
		return 80;
	}

	public static void main(String[] args) {
		new TestServer();
	}
}

Specifying Generic Body/Return Types:


/**
 * SERVER CODE
 */
this.addEndpoint(HttpMethod.POST, "/GetEmployee", JsonObject.class, (request)->{
	JsonObject payload = request.getBody();
	int id = payload.get("id").getAsInt();
	
	String[] names = {
			"Frank",
			"Jeff",
			"Oliver",
			"Maxwell"
	};
	
	JsonObject response = new JsonObject();
	response.addProperty("id", id);
	response.addProperty("name", names[id-1]);
	
	return new ResponseEntity<JsonObject>(HttpStatus.OK, response);
});

/**
 * CLIENTCODE
 */
JsonObject body = new JsonObject();
body.addProperty("id", 1);
RequestEntity<JsonObject> request = new RequestEntity<>(HttpMethod.POST, body);
request.exchangeAsync("http://localhost/GetEmployee", JsonObject.class, (response)->{
	JsonObject payload = response.getBody();
	System.out.println("Employee data: ");
	System.out.println("\tid: " + payload.get("id").getAsInt());
	System.out.println("\tname: " + payload.get("name"));
});

DTO Serialization (using Gson):

public class TestDTO {

	public TestDTO() throws MalformedURLException {
		// Create payload
		JsonObject body = new JsonObject();
		body.addProperty("id", 1);
		
		// Create request object
		RequestEntity<JsonObject> request = new RequestEntity<>(HttpMethod.POST, body);
		
		// Send request to server
		request.exchangeAsync("http://localhost/GetEmployee", Employee.class, (response)->{
			Employee employee = response.getBody();
			System.out.println("Employee data: ");
			System.out.println("\tid: " + employee.getId());
			System.out.println("\tname: " + employee.getName());
		});
	}

	public static void main(String[] args) throws MalformedURLException, IOException {
		new TestDTO();
	}
}

/**
 * DTO used to represent employee information sent from server.
 */
class Employee {
	private int id;
	
	private String name;
	
	public Employee() {
		//
	}
	
	public int getId() {
		return id;
	}
	
	public void setId(int id) {
		this.id = id;
	}
	
	public String getName() {
		return this.name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
}

Also works like a webserver:

/**
 * SERVER CODE
 */
this.addEndpoint(HttpMethod.GET, "/", MediaType.TEXT_HTML, (request)->{
	return new ResponseEntity<String>(HttpStatus.OK, "<h1>Index! Welcome to JREST!</h1>");
});

Actually… wouldn’t it be better to just use a string for media types instead of having predefined options? There could be all kinds of use cases where a custom MIME type is needed.
Even if you think it would be better to keep the MediaType type to avoid ambiguity, why not at least expose its constructor?
Regardless, having those default constants available at all is probably convenient.

The same goes for the HttpStatus enum, though to a lesser extent. Depending on the intended way to use this it might actually be more convenient to do it like you do, but there are apparently a whole bunch of unofficial status codes that are also in use for all kinds of purposes.
Some companies supposedly even use custom 9xx codes internally for some reason.

Predefined options are important because they change how data is sent/received/processed within the application. I wouldn’t mind exposing the constructor though for custom ones.

Perhaps I am misunderstanding you and you mean the library treats them differently somehow; if so, my bad.

To my knowledge, both are metainformation, which both the server and client application do use, yes, so it is important to handle it. At the same time, they can have any value, not just the ones you offered as options; just see the link I included for example.

My point was not that you should not handle them; my point was that you should not limit developers using your library in what values they are allowed to use.

When using a whitelist for such options, you are almost certainly going to miss a number of use cases which would be perfectly compatible with the rest of the library otherwise. That is why I am recommending you avoid whitelists.

Yes. The library treats them differently. It has to for things like POJO conversion or packing encoded url information in the request.

I am having trouble understanding how these are not mutually exclusive statements. If it is no problem to allow any value (i.e. exposing the constructor) and all I am asking for is to allow any value, how can any of what I am asking for be invalid?

Again, just avoid having whitelists that cannot be extended without modifying the library. That is all I am asking for.

Like I’ve said, certain media types have to be taken into account to pass the data from point-to-point differently. If I mark a receiving data as JSON, it needs to be automatically converted into a map or list upon arrival. If I mark an outgoing data as URL_FORM_ENCODED I need to handle differently how the data is packed into the payload. Because these are things that other restful libraries already do as well. Additionally, I’m modeling it after spring, which is why I am using pre-defined types.

Though I can still expose the constructor of MediaType/HttpCode to allow for custom ones to be sent, but you’ll have to handle converting the data on your own. One of the benefits do strict typing is I can do some real useful auto-conversions.

Some updates since last year:

  • Created Endpoint builder class
  • RequestEntity#exchange() can now receive data in a different type than was sent
  • Endpoints can now return different types of data than was received
  • Fixed a NPE for when when data was expected to be returned from an endpoint, but nothing was sent.