App Examples¶
A few examples are included here to highlight usage scenarios. The syntax used will be JavaScript although the same the Java implementation would almost literary be the same.
Hello World App¶
This is just a simple application which responds with the current time when the root context is accessed.
Create a new project directory and add a
lib
folder to it as well.:mkdir -p my-app/lib;
Download the
zesty-router[version].jar
and place it in the lib folder.cd into my-app folder and create a new file,
index.js
.In the
index.js
file, import theAppProvider
class.:let zesty = Java.type('com.practicaldime.router.base.AppProvider');
Initialize the application using some initial configuration.:
let app = zesty.provide({}); print('zesty app is configured');
Create a router to add handlers.:
let router = app.router();
Add a function to fetch the current time. For a little bit or fun, let’s also format the time.:
let Date = Java.type('java.util.Date'); let DateFormat = Java.type('java.text.SimpleDateFormat'); function now() { return new DateFormat("hh:mm:ss a").format(new Date()); }
Add a route to handle the root context.:
router.get('/', function (req, res) { res.send(app.status().concat(" @ ").concat(now())); });
Configure the host and port to listen for requests.:
let port = 8080, host = 'localhost'; router.listen(port, host, function(result){ print(result); });
Start the application and start serving time.:
jjs --language=es6 -ot -scripting -J-Djava.class.path=./lib/zesty-router-0.1.0-shaded.jar index.js
Simple REST App¶
Let’s create a file simple_rest.js
and begin by creating a simple database access class. Since nashorn
does not use the class
keyword, we will use the function
syntax instead.:
let AtomicInteger = Java.type('java.util.concurrent.atomic.AtomicInteger');
function UserDao() {
this.users = {
0: {name: "James", email: "james@jjs.io", id: 0},
1: {name: "Steve", email: "steve@jjs.io", id: 1},
2: {name: "Carol", email: "carol@jjs.io", id: 2},
3: {name: "Becky", email: "becky@jjs.io", id: 3}
}
this.lastId = new AtomicInteger(3); //this.users.size() - 1
this.save = (name, email) => {
let id = this.lastId.incrementAndGet()
this.users[id] = {name: name, email: email, id: id}
return this.users[id];
}
this.findById = (id) => {
return this.users[id]
}
this.findByEmail = (email) => {
return Object.values(this.users).find(it => it.email == email )
}
this.update = (id, name, email) => {
this.users[id] = {name: name, email: email, id: id}
}
this.delete = (id) => {
delete this.users[id]
}
}
Next, let’s create the API service.:
let dao = new UserDao();
let zesty = Java.type('com.practicaldime.router.base.AppProvider');
let app = zesty.provide({
appctx: '/users'
});
let router = app.router();
router.get('/', function (req, res) {
res.json(dao.users);
});
router.get('/{id}', function (req, res) {
let id = req.param('id');
res.json(dao.findById(parseInt(id)))
});
router.get('/email/{email}', function (req, res) {
let email = req.param('email');
res.json(dao.findByEmail(email));
});
router.post('/create', function (req, res) {
let name = req.param('name');
let email = req.param('email');
dao.save(name, email);
res.status(201);
});
router.put('/update/{id}', function (req, res) {
let id = req.param('id')
let name = req.param('name');
let email = req.param('email');
dao.update(parseInt(id), name, email);
res.status(204);
});
router.delete('/delete/{id}', function (req, res) {
let id = req.param('id')
dao.delete(parseInt(id))
res.status(205);
});
let port = 8080, host = 'localhost';
router.listen(port, host, function(result){
print(result);
});
Start the application and listen for requests.:
jjs --language=es6 -ot -scripting -J-Dlogback.configurationFile=../lib/app-logback.xml \
-J-Djava.class.path=../lib/zesty-router-0.1.0-shaded.jar simple_rest.js
For comparison, the Java equilavent of simple_rest.js
would be.:
public class SimpleRest {
static class User {
private int id;
private String name;
private String email;
public User(String name, String email, int id) {
super();
this.id = id;
this.name = name;
this.email = email;
}
//omitted getters and setters
}
static class UserDao {
private AtomicInteger lastId;
private Map<Integer, User> users = new HashMap<>();
public UserDao() {
users.put(0, new User("James", "james@jjs.io", 0));
users.put(1, new User("Steve", "steve@jjs.io", 1));
users.put(2, new User("Carol", "carol@jjs.io", 2));
users.put(3, new User("Becky", "becky@jjs.io", 3));
lastId = new AtomicInteger(users.size() - 1);
}
public Map<Integer, User> all(){
return this.users;
}
public void save(String name, String email) {
int id = lastId.incrementAndGet();
users.put(id, new User(name, email, id));
}
public User findById(int id) {
return this.users.get(id);
}
public User findByEmail(String email){
return users.values().stream()
.filter(user -> user.getEmail().equals(email))
.findFirst()
.orElse(null);
}
public void update(int id, String name, String email) {
users.put(id, new User(name, email, id));
}
public void delete(int id) {
users.remove(id);
}
}
public static void main(String...args) {
UserDao dao = new UserDao();
Map<String, String> config = new HashMap<>();
config.put("appctx", "/users");
AppServer app = AppProvider.provide(config);
app.router()
.get("/", (req, res) -> {
res.json(dao.all());
return null;
})
.get("/{id}", (req, res) -> {
String id = req.param("id");
res.json(dao.findById(Integer.valueOf(id)));
return null;
})
.get("/email/{email}", (req, res) -> {
String email = req.param("email");
res.json(dao.findByEmail(email));
return null;
})
.post("/create", (req, res) -> {
String name = req.param("name");
String email = req.param("email");
dao.save(name, email);
res.status(201);
return null;
})
.put("/update/{id}", (req, res) -> {
String id = req.param("id");
String name = req.param("name");
String email = req.param("email");
dao.update(Integer.valueOf(id), name, email);
res.status(204);
return null;
})
.delete("/delete/{id}", (req, res) -> {
String id = req.param("id");
dao.delete(Integer.valueOf(id));
res.status(205);
return null;
})
.listen(8080, "localhost", (result) ->{
System.out.println(result);
});
}
}
Adding a Page¶
Let’s now create a home page for the simple_rest
app we have going. To do this, create a folder www
in the project’s
root directory, and add a new file index.html
.:
<!DOCTYPE html>
<html>
<head>
<title>Index Page</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0px;
padding: 0px;
}
#wrapper {
width: 900px;
margin: 0px auto;
display: grid;
justify-content: center;
align-content: center;
grid-template-columns: repeat(3, 20vmin);
grid-template-rows: repeat(5, 20vmin);
grid-gap: 10px;
}
#wrapper .content {
display: grid;
align-content: center;
justify-content: center;
}
#wrapper .content:nth-child(even) {background: #eee}
#wrapper .content:nth-child(odd) {background: #ccc}
</style>
</head>
<body>
<div id="wrapper">
<div class="content">A</div>
<div class="content">B</div>
<div class="content">C</div>
<div class="content">D</div>
<div class="content">E</div>
<div class="content">F</div>
</div>
</body>
</html>
In the sample_rest.js
file, configure the assets parameters in the AppProvider
.:
let app = zesty.provide({
appctx: '/users',
assets: 'www'
});
Restart the application and navigate to the root context http://localhost:8080
. Before adding the index.html
,
the response was a 404 - Not found
error. Now you should expect to see the index page.
A Freemarker Template¶
The previous example used a plain html
page. This example uses a Freemarker template to display the users from the
simple_rest
application. Let’s create one. Copy the index.html
file and rename it to index.ftl
.
This will be layout page for other pages. Let’s begin with extracting the css into a new file, index.css
:
* {
margin: 0px;
padding: 0px;
}
#wrapper {
width: 900px;
margin: 0px auto;
display: grid;
justify-content: center;
align-content: center;
grid-template-columns: repeat(3, 40vmin);
grid-template-rows: repeat(5, 40vmin);
grid-gap: 10px;
}
#wrapper .content {
display: grid;
align-content: center;
justify-content: center;
}
#wrapper .content:nth-child(even) {background: #eee}
#wrapper .content:nth-child(odd) {background: #ccc}
Refactor the index.ftl
page to make it a macro.:
<#macro page>
<!DOCTYPE html>
<html>
<head>
<title>Index Page</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link type="text/css" rel="stylesheet" href="/index.css">
</head>
<body>
<div id="wrapper">
<#nested>
</div>
</body>
</html>
</#macro>
Now create a template for displaying user data, and call it users.ftl
.:
<#import "index.ftl" as u>
<@u.page>
<#list users?values as user>
<div class="content" data-key="${user.id}">
<p class="name">${user.name}</p>
<p class="email">${user.email}</p>
<p class="link">
<a href="#" onclick="removeUser(event, '${user.id}')">delete</a>
</p>
</div>
</#list>
<script>
function removeUser(e, id){
e.preventDefault();
fetch('/users/delete/' + id, {method: 'DELETE'})
.then(res=> {
let user = document.querySelector("[data-key='" + id + "']");
user.remove();
})
.catch(err=>console.log(err));
}
</script>
</@u.page>
This template iterates over the values of the users’ map passed from the calling function, and for each user it creates a corresponding
user element. Each user element also contains a delete
link. The template contains a script which removes the corresponding user
element from the page when clicked. Now create a route to render this users.ftl
page on the /users
context.:
router.get('/', function (req, res) {
res.render('users', {users: dao.users});
});
And finally, let’s configure the AppProvider
to be aware of the view engine.:
let app = zesty.provide({
appctx: '/users',
assets: 'www',
engine: "freemarker"
});
Restart the application and navigate to the root context http://localhost:8080/users
.
Adding Submit Form¶
Let’s start by splitting the simple_rest.js
file into two and call the second file simple_repo.js
. This way,
have the repository separate from the rest endpoints, and thereby both file can evolve independently. Let’s slightly refector
the simple_repo.js
source code so that it is importable in other modules.:
let Dao = {};
;(function(){
let AtomicInteger = Java.type('java.util.concurrent.atomic.AtomicInteger');
function UserDao() {
this.users = {
0: {name: "James", email: "james@jjs.io", id: 0},
1: {name: "Steve", email: "steve@jjs.io", id: 1},
2: {name: "Carol", email: "carol@jjs.io", id: 2},
3: {name: "Becky", email: "becky@jjs.io", id: 3}
}
this.lastId = new AtomicInteger(3); //this.users.size() - 1
this.save = (name, email) => {
let id = this.lastId.incrementAndGet()
this.users[id] = {name: name, email: email, id: id}
return this.users[id];
}
this.findById = (id) => {
return this.users[id]
}
this.findByEmail = (email) => {
return Object.values(this.users).find(it => it.email == email )
}
this.update = (id, name, email) => {
this.users[id] = {name: name, email: email, id: id}
}
this.delete = (id) => {
delete this.users[id]
}
}
//export the class through the Dao scope
Dao.UserDao = UserDao;
})();
With the split done, now simply import the repository to be used by the endpoints in simple_rest.js
.:
load('./simple_repo.js');
let dao = new Dao.UserDao();
let zesty = Java.type('com.practicaldime.router.base.AppProvider');
let app = zesty.provide({
appctx: '/users',
assets: 'www',
engine: "freemarker"
});
let router = app.router();
router.get('/', function (req, res) {
res.render('users', {users: dao.users});
});
router.get('/{id}', function (req, res) {
let id = req.param('id');
res.json(dao.findById(parseInt(id)))
});
router.get('/email/{email}', function (req, res) {
let email = req.param('email');
res.json(dao.findByEmail(email));
});
router.post('/create', function (req, res) {
let name = req.param('name');
let email = req.param('email');
dao.save(name, email);
res.status(201);
});
router.put('/update/{id}', function (req, res) {
let id = req.param('id')
let name = req.param('name');
let email = req.param('email');
dao.update(parseInt(id), name, email);
res.status(204);
});
router.delete('/delete/{id}', function (req, res) {
let id = req.param('id')
dao.delete(parseInt(id))
res.status(205);
});
let port = 8080, host = 'localhost';
router.listen(port, host, function(result){
print(result);
});
With this done, we need to slightly refactor the users.ftl
file so that we can have a separate template for a single user.
Call this new template user.ftl
and add this markup.:
<div class="content" data-key="${user.id}">
<p class="name">${user.name}</p>
<p class="email">${user.email}</p>
<p class="link">
<a href="#" onclick="removeUser(event, '${user.id}')">delete</a>
</p>
</div>
This template expects a user
object in its context and renders the user attributes in it. This template will come in handy when
creating a new user or even editing an existing user. Now we need to import and use this template in the users.ftl
file. And
while doing so, add another block element for the form to submit user data for persistence in the repository.:
<#import "index.ftl" as u>
<@u.page>
<div class="content" data-key="create">
<form action="/users/create" onsubmit="saveUser(event, this)">
<input type="hidden" name="id"/>
<div class="input-row"><span class="title">Name</span><input type="text" name="name"/></div>
<div class="input-row"><span class="title">Email</span><input type="text" name="email"/></div>
<div class="input-row"><input class="button" type="submit" value="Save"/></div>
</form>
</div>
<#list users?values as user>
<#include "user.ftl"/>
</#list>
<script>
function removeUser(e, id){
e.preventDefault();
fetch('/users/delete/' + id, {method: 'DELETE'})
.then(res=> {
let user = document.querySelector("[data-key='" + id + "']");
user.remove();
})
.catch(err=>console.log(err));
}
</script>
</@u.page>
The new data entry component we added contains a form which references a saveUser(event, this)
method.
Add the implementation in the script
section beneath the removeUser(e, id)
function.:
function saveUser(e, form){
e.preventDefault();
let id = form.get('id');
return id? updateUser(id, form) : createUser(form);
}
function createUser(form){
fetch('/user/create', {method: 'POST', body: form})
.then(res=>{})
.catch(err=>{})
}
function updateUser(id, form){
fetch('/user/update/' + id, {method: 'PUT', body: form})
.then(res=>{})
.catch(err=>{})
}
We’ll add the function bodies in a moment. But before that, add some styling for the form component we just added.:
#wrapper .content form {
padding: 5px;
}
#wrapper .content form .input-row {
display: flex;
padding: 5px;
}
#wrapper .content form .title{
margin: 5px;
}
#wrapper .content form input[type=text]{
padding: 5px 10px;
border-radius: 10px;
line-height: 1.5em;
}
#wrapper .content form input.button{
padding: 8px 10px;
min-width: 70px;
margin: 5px;
}
To accomodate these new features, we need to slightly modify the repository in simple_repo.js
. Let’s begin with the
router.get('/{id}'...)
function. Instead of returning a json object, let’s have it return a rendered user fragment.
This will be useful for both create
and update
operations.:
router.get('/{id}', function (req, res) {
let id = req.param('id');
let user = dao.findById(parseInt(id));
res.render('user', user);
});
Next, modify the router.post('/create'...)
to redirect after POST instead of returning just the status code.:
router.post('/create', function (req, res) {
let name = req.param('name');
let email = req.param('email');
let user = dao.save(name, email);
res.redirect(app.resolve("/" + user.id));
});
Next, modify the router.put('/update/{id}'...)
function to return a rendered component directly. Since the behaviour
of a redirect on PUT or DELETE is not standard across all servers, and because the update does not create a new resource, it
makes more sense to respond with the markup instead of redirecting like we did with POST.:
router.put('/update/{id}', function (req, res) {
let id = req.param('id')
let name = req.param('name');
let email = req.param('email');
dao.update(parseInt(id), name, email);
res.render('user', {user: {id, name, email}});
});
Now let’s add the body for the createUser(form)
function.:
function createUser(form){
const options = {
method: 'post',
headers: {
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: encodeFormData(form)
}
fetch('/users/create', options)
.then(res=> res.text())
.then(html=>{
let parent = document.getElementById("wrapper");
let element = htmlToElement(html);
parent.appendChild(element);
})
.catch(err=>{
console.log(err)
})
}
We added an options parameter to describe the request data. We called a new method encodeFormData()
to convert the form data
into a form-urlencoded
string. Without this step, the data would be sent as multipart-data
which is not the format
we want. In the response, we also called a new method htmlToElement()
which parses the response into a document element. The
implementations for these two helper methods is shown below.:
function encodeFormData(data){
var urlEncodedData = "";
var urlEncodedDataPairs = [];
var name;
for(const name of data.keys()) {
urlEncodedDataPairs.push(encodeURIComponent(name) + '=' + encodeURIComponent(data.get(name)));
}
return urlEncodedDataPairs.join('&').replace(/%20/g, '+');
}
function htmlToElement(html) {
var template = document.createElement('template');
template.innerHTML = html.trim();
return template.content.firstChild;
}
Now let’s add the body for the updateUser(id, form)
function. Just like we did with the createUser
method, we define
an options parameter to describe the request data, and we use both the encodeFormData()
and htmlToElement()
methods in
the same way. For the response, this time we replace the existing component with the updated one instead of appending a new one.:
function updateUser(id, form){
const options = {
method: 'put',
headers: {
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: encodeFormData(form)
}
fetch('/users/update/' + id, options)
.then(res=> res.text())
.then(html=>{
let parent = document.getElementById("wrapper");
let element = htmlToElement(html);
let target = parent.querySelector("[data-key='" + id + "']");
parent.replaceChild(element, target);
resetForm();
})
.catch(err=>{
console.log(err)
})
}
For the updateUser()
to work, we need a way to select a user whom to edit. So we will add a link to the user component that selects
the clicked user . In the user.ftl
, add an edit link like shown below.:
<p class="link">
<a href="#" onclick="selectUser(event, '${user.id}', '${user.name}', '${user.email}')">edit</a>
<a href="#" onclick="removeUser(event, '${user.id}')">delete</a>
</p>
Now we need to add a selectUser(event, id, name, email)
method in the script section of users.ftl
. For completeness, let’s
also add another method, resetForm()
to clear the form when an update is completed.:
function selectUser(e, id, name, email){
e.preventDefault();
let form = document.querySelector("[data-key='edit'] form");
form.elements["id"].value = id;
form.elements["name"].value = name;
form.elements["email"].value = email;
}
function resetForm(){
let form = document.querySelector("[data-key='edit'] form");
form.elements["id"].value = "";
form.elements["name"].value = "";
form.elements["email"].value = "";
}
This method populates the form component which makes the Save operation an update instead of a create operation. At this
point, restart the application again and navigate to the /users
context http://localhost:8080/users.
**Please check again soon. The material is continually getting updated**