ActiveJ | To-Do list app using React

Purpose

In this tutorial we will create a To-Do List app using ActiveJ and React. We will explore how to integrate React in an ActiveJ project and how to simply manage routing using the HTTP module. You can find full example sources on GitHub.

Here we will consider only the main app class - ApplicationLauncher. It uses ActiveJ HttpServerLauncher and AsyncServlet classes for setting up an embedded application server. With this approach, you can create servers with no XML configurations or third-party dependencies. Moreover, HttpServerLauncher will automatically take care of launching, running and stopping the application. You’ll only need to provide launcher with servlets.

Creating Launcher

ApplicationLauncher is the main class of the program. Besides launching the application, it also handles routing and most of the corresponding logic. We will use ActiveJ HttpServerLauncher and extend it:

public final class ApplicationLauncher extends HttpServerLauncher {

	private static final StructuredCodec<Plan> PLAN_CODEC = object(Plan::new,
			"text", Plan::getText, STRING_CODEC,
			"isComplete", Plan::isComplete, BOOLEAN_CODEC);

	private static final StructuredCodec<Record> RECORD_CODEC = object(Record::new,
			"title", Record::getTitle, STRING_CODEC,
			"plans", Record::getPlans, ofList(PLAN_CODEC));

	@Provides
	RecordDAO recordRepo() {
		return new RecordImplDAO();
	}

	@Provides
	Executor executor() {
		return newSingleThreadExecutor();
	}

}

First, we define codecs for our two entities: Plan and Record. These codecs will help us to encode/decode Plan and Record from/to JSONs to communicate with TodoService.js.

Method object returns a new StructuredCodec and, in the case of Plan and Record entities, that requires the following parameters:

  • TupleParser2 constructor - basically a constructor of your class with 2 parameters. There are several predefined TupleParsers for up to 6 parameters.
  • String field1 - the first field of the encoded/decoded class
  • Function getter1 - getter of field1
  • StructuredCodec codec1 - codec for field1 (depends on the type of the field, for example, STRING_CODEC, BOOLEAN_CODEC)
  • String field2 - another field of the class
  • Function getter2 - getter of field2
  • StructuredCodec codec1 - codec for field2

Next, we provided RecordDAO with the application business logic and Executor which is needed for our AsyncServlet. AsyncServlet loads static content from /build directory and takes care of the routing:

@Provides
AsyncServlet servlet(Executor executor, RecordDAO recordDAO) {
	return RoutingServlet.create()
			.map("/*", StaticServlet.ofClassPath(executor, "build/")
					.withIndexHtml())

}

Routing in ActiveJ HTTP module resembles Express approach. Method map(@Nullable HttpMethod method, String path, AsyncServlet servlet) adds routes to the RoutingServlet:

  • method (optional) is one of the HTTP methods (GET, POST, etc.)
  • path is the path on the server
  • servlet defines the logic of request processing. If you need to get some data from the request while processing you can use:
    • request.getPathParameter(String key)/request.getQueryParameter(String key) (see example of query parameter usage) to provide the key of the needed parameter and receive back a corresponding String
    • request.getPostParameters() to get a Map of all request parameters

Pay attention to the * in the provided request. It states that whichever path until the next / is received, it will be processed by our static servlet, which uploads static content from /build directory.

Servlet should be able to add a new record, get all available records, delete record by its id and also mark plans of particular record as completed or not, so we provide corresponding routing:

.map(POST, "/add", loadBody()
		.serve(request -> {
			ByteBuf body = request.getBody();
			try {
				Record record = JsonUtils.fromJson(RECORD_CODEC, body.getString(UTF_8));
				recordDAO.add(record);
				return HttpResponse.ok200();
			} catch (ParseException e) {
				return HttpResponse.ofCode(400);
			}
		}))
.map(GET, "/get/all", request -> {
	Map<Integer, Record> records = recordDAO.findAll();
	return HttpResponse.ok200()
			.withJson(JsonUtils.toJson(ofMap(INT_CODEC, RECORD_CODEC), records));
})
//[START REGION_4]
.map(GET, "/delete/:recordId", request -> {
	int id = parseInt(request.getPathParameter("recordId"));
	recordDAO.delete(id);
	return HttpResponse.ok200();
})
//[END REGION_4]
.map(GET, "/toggle/:recordId/:planId", request -> {
	int id = parseInt(request.getPathParameter("recordId"));
	int planId = parseInt(request.getPathParameter("planId"));

	Record record = recordDAO.find(id);
	Plan plan = record.getPlans().get(planId);
	plan.toggle();
	return HttpResponse.ok200();
});

Pay attention to the requests with :, for example:

.map(GET, "/delete/:recordId", request -> {
	int id = parseInt(request.getPathParameter("recordId"));
	recordDAO.delete(id);
	return HttpResponse.ok200();
})

: states that the following characters until the next / is a variable whose keyword, in this case, is recordId.

Running the application

To run the example clone ActiveJ and import it as a Maven project. Check out branch v2.2. Before running the example, build the project (Ctrl + F9 for IntelliJ IDEA).

Then, run the following command in activej -> examples -> tutorials -> react-integration-2 -> front directory in terminal:

$ npm i
$ npm run-script build

Open ApplicationLauncher and run its main method. Then open your favourite browser and go to localhost:8080. Try to add and delete some tasks or mark them as completed.