A todo app example using express, lowdb, and much more.
So I have been working with express.js for a while now when it comes to making simple demos, but now I think it is time to start making something that is a full working project of some kind. Often people start with a simple todo list project of some kind, so maybe that will do for now. I do not have to make this the kind of project that I will devote a few years of my life to, it can just be a good start. In this post I will be writing about this first express.js project, and if all goes well maybe this will not be the last post like this, as I progress into something else that is more interesting.
1 - What to know before continuing
This is a post on an example of a full stack web application built using node.js, express.js, and a whole bunch of other modules. This is not at all a getting started post on express.js, javaScript, html, css, and so forth. If you are new to this sort of thing it might be best to start at my main post on express.js, as this is a more advanced post on full stack web development using express.js.
1.1 - This is the first release I am writing about
This is the first release that I am writing about in this post express_todo 0.0.125. There might be a 1.x in the future as there is a lot about this project that I am not satisfied with. However I am interested in progressing into more interesting projects as well, so that might not come to pass. So in other words, this project is not at all production ready, and if you are going to use it I would only do so locally.
2 - install, or setup
Because this has turned out to be a complex project I have made a repo on my github page. So If for some reason you want to install this locally you can by cloning it down, and doing an npm install to install all the dependencies for it.
2.1 - install by cloning the repo
So one way to quickly reproduce what I am wrting about here is to just clone down what I have made with git clone, make sure you are using the version that I am writing about (0.0.125), and then install the dependencies with an npm install.
Once everything is installed you would just need to call node app to start the main app.js file, and if all goes well you will be able to use the app when you navigate to localhost:8080 in a web browser.
2.2 - Reproducing from scratch
If you want to reproduce from scratch there are a few things to install, and study if you are not familiar with them.
However you might start out like this.
1
2
3
4
5
6
7
8
9
10
$ mkdir express_todo
$ cd express_todo
$ npm init
$ npm install ejs@2.6.1 --save
$ npm install express@4.16.3 --save
$ npm install fs-extra@6.0.1 --save
$ npm install js-yaml@3.12.0 --save
$ npm install lodash@4.17.10 --save
$ npm install lowdb@1.0.0 --save
$ npm install shortid@2.2.8 --save
3 - At the root
At the root of the project folder is a few files of interest, like any other express.js project or demo of mine there is the main app.js file, also there will be a conf.yaml file as well for some app settings. There are also many folders of interest that lead into many other folders and files that compose both a front end, and back end system.
3.2 - The main app.js file
The main app.js file might always be a good starting location, as this is the file that is called with node to start the app. It is here that the main app object instance is, I am also calling a conf.js file in the lib folder that makes a conf.yaml file, or get settings from it that are used to set app settings like what port or theme to use.
I am also setting up all my static paths here as well as using some routes defined in the routes folder, that in turn also use some built in middleware methods.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
let express = require('express'),
path = require('path'),
app = express();
// use lib/conf.js to load app settings from conf.yaml
Of course this is also where I am calling app.listen to start the project on a port that is in the port app setting.
3.1 - config.yaml
So the config.yaml is a file that will be created when starting express_todo for the first time. as of this writing there are just two settings the port, and the theme. As of this writing the only value you might want to change is the port, as there is only one theme.
4 - The /lib folder
This lib folder contains tow files, conf.js that is used to make or read the main conf.yaml file that is used to store app settings, and the db_lists file that is used to help work with the lowdb powered database.
4.1 - conf.js
So the conf.js file reads the main conf.yaml file and sets app settings for the main instance of the app object. In the event that the conf.yaml file is not there one is created, and set up with some hard coded default settings.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
let express = require('express'),
fs = require('fs-extra'),
_ = require('lodash'),
yaml = require('js-yaml'),
path = require('path');
// set settings to the given app with the given settings object
let setWithYAML = function (app, root, yamlConf) {
// load yaml
let setObj = yaml.safeLoad(yamlConf.toString());
// set settings
_.each(setObj, function (val, key) {
app.set(key, val);
});
// insure port is set to the PORT environment variable if present
This file might be a bit overkill at the moment, but if I do take the time to expand this project, then there might come a time where I would want a setup like this.
4.2 - db_lists.js
This is the lib that I am using to interact with my database. For this simple todo app I am using lowdb for a database solution, not the best choice for a production app, but this is no production app. I do like the simplicity of lowdb, and for simple projects like this that I do not intent to deploy, it gets the job done.
This is used by my middleware functions to handle everything with respect to the lists.json file in the db folder. Also any additional future middleware will use this as well when it comes to doing anything with the list data.
5 - The /public folder
So like many of my express.js projects so far there is a public folder. I put this folder in as a way to serve up some static assets that will be shared across what might eventually be more than one theme. For the moment I am using it as a way to just host a single javaScript file that acts as a kind of api to access everything of interest in the back end.
5.1 /public/js/list_client.js
So this is a javaScript file that provides a simple custom trailered http client using XMLHttprequest, and a bunch of methods that can be called to make certain kinds of requests from the front end. Requests for a certain list if I know the id, and making post requests for new lists, and items.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
var get = function (id) {
returndocument.getElementById(id);
};
// list client.
var lc = (function () {
// simple, custom, http client using XMLHttpRequest
var http = function (obj) {
obj = obj || {};
// defaults to making GET requests to the /list path
this.method = obj.method || 'GET';
this.path = obj.path || '/list';
this.body = obj.body || null;
this.onDone = obj.onDone || function () {
console.log(this.response);
};
this.onFail = obj.onFail || function () {
console.log(this.response);
};
// start the request
var xhr = this.xhr = new XMLHttpRequest();
xhr.open(this.method, this.path);
// with this client all GET,and POST request should be for JSON
So the routes folder is a way to help break down the many paths that are defined by this project. For the most part when I think about it I am going to want to have a path that has to do with editing a list, and another path that has to do with creating, deleting, and getting a main index of lists. So in this folder there is an edit.js, and list.js files, as well as a middleare folder that has all the middleware functions that are used by these two paths.
6.1 - /routes/edit.js
Here in the edit.js file I am setting what should be done for get, and post requests to the /edit path. This path is used by the client to edit a list that was created before hand.
like with edit.js I am using many middleware methods in an additional folder that I placed in the routes folder.
6.3 - The middleware at the /routes/mw folder
here I have a much of middleware files that I use with the /edit and /list paths as a way of breaking things down more, so they are easier to understand, and manage.
6.3.1 - edit_get.js
So making a get request to the /edit path will result in server side rendering, and what will be rendered depends on the query string given.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// a middleware that starts a render object
let dbLists = require('../../lib/db_lists');
module.exports = [
// set up a standard render object
require('./setobj_rend'),
// make sure we are using the edit layout
function (req, res, next) {
// use edit layout
req.rend.layout = 'edit';
next();
},
// render list of lists, if no listId is given in the query string
function (req, res, next) {
if (req.query.l === undefined) {
dbLists.readLists().then(function (lists) {
req.rend.lists = lists.get('lists').value();
res.render(req.rend.main, req.rend);
}).catch (function () {
res.render(req.rend.main, req.rend);
});
} else {
// else we where given a list id so set the list id, and continue.
I made this middleware as a way to setup a standard object that will be passed to ejs wen rendering templates.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// a middleware that starts a render object
module.exports = function (req, res, next) {
req.rend = {
main: 'index', // main ejs file to use in the root of the theme
layout: 'index', // layout ejs file to use
listId: null,
itemId: null,
lists: [],
list: {},
item: {}
};
next();
};
6.3.3 - setobj_postres.js
This one sets a standard object that is send for responses to post requests.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// a middleware that starts a render object
module.exports = function (req, res, next) {
// set defaults for an standard object
// that will be send back for a post
// request
req.postRes = {
success: false,
body: req.body,
mess: '',
eMess: '',
list: [],
item: {}
};
next();
};
6.3.4 - check_body.js
As the name suggests this middeware just checks for a body, and makes sure a mode is set as well. If something is missing it responds, else it does nothing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// check body
module.exports = function (req, res, next) {
// body must be there,
if (req.body) {
// and the body must have a mode property
if (req.body.mode) {
// then we are good to continue
next();
} else {
// respond with no mode mess
res.json({
success: false,
mess: 'no mode.',
body: req.body
});
}
} else {
// respond with no body mess
res.json({
success: false,
mess: 'no body was parsed.'
});
}
};
6.3.5 - check_fail.js
This is a standard middleware that is used at the end of an array of middlewares as a sort of end of the line middleware if nothing happens.
1
2
3
4
5
6
7
// end of the line.
module.exports = function (req, res) {
req.postRes.mess = 'post recived, but nothing was done. Check the given body';
res.json(req.postRes);
};
6.3.6 - item_add.js
adds an item to a list.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let dbLists = require('../../lib/db_lists');
// check body
module.exports = function (req, res, next) {
if (req.body.mode === 'add_list_item' && req.body.item && req.body.listId) {
This is the folder that will store the themes for the project. For now there is only one theme, but if I continue developing this I will likely experiment with different front end solutions. The one and only theme in use so far is called landscape, and it is nothing to write home about. I just wanted to quickly slap something together that just works for this first release.
7.1 - The Landscape theme
For the landscape theme I just put together something that is composed of just my own vanilla javaScript, css, and ejs markup.
7.1.2 - landscape/css/style.css
There is a css path for the theme, but for now there are only two classes so there is not much to write about.
1
2
3
4
5
6
7
8
9
10
11
.item_done{
text-decoration: line-through
}
.item_not_done{
text-decoration: none
}
7.1.3 - landscape/js/create.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// when create button is clicked
get('create_submit').addEventListener('click', function (e) {
// get all inputs with class 'meta'
var nodes = get('create').querySelectorAll('.meta'),
// set up a new body to send
body = {};
// forEach 'meta' input
[].forEach.call(nodes, function (el) {
// make it part of body
body[el.name] = el.value;
});
// use list clients createList Method to send the body
lc.createList({
body: body,
onDone: function () {
var res = JSON.parse(this.response);
// redirect to origin
window.location.href = '/edit?l=' + res.list.id;
}
});
});
7.1.4 - landscape/js/edit.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// edit path client for 'landscape' theme
var reload = function (noListId) {
var param = '';
if (!noListId) {
param = '?l=' + get('listid').innerHTML;
}
window.location.href = '/edit' + param;
};
// when a list item is clicked
var onItemClick = function () {
console.log('li element clicked');
};
// when a done button is clicked
var onDoneClick = function (e, done) {
console.log('Done button clicked');
var li = e.target.parentElement,
itemId = li.id.replace(/^item_/, ''),
listId = get('listid').innerHTML;
done = done || function () {};
new lc.http({
path: '/edit',
method: 'POST',
body: JSON.stringify({
mode: 'edit_list_item',
listId: listId,
itemId: itemId,
toggleDone: true
}),
onDone: done
});
};
// when the delete button is clieck
var onDeleteClick = function (e, done) {
var itemId = e.target.dataset.itemId,
listId = get('listid').innerHTML;
done = done || function () {};
new lc.http({
path: '/edit',
method: 'POST',
body: JSON.stringify({
mode: 'delete_list_item',
listId: listId,
itemId: itemId
}),
onDone: done
});
}
if (get('listid')) {
// for each hard coded list item
[].forEach.call(document.querySelectorAll('.button_done'), function (el) {
el.addEventListener('click', function (e) {
onDoneClick(e, function () {
reload();
});
});
});
// for each delete button
[].forEach.call(document.querySelectorAll('.button_delete'), function (el) {
el.addEventListener('click', function (e) {
onDeleteClick(e, function () {
reload();
});
});
});
// if add item button is clicked
get('newitem_submit').addEventListener('click', function () {
var text = get('newitem_text').value;
if (text !== '') {
lc.addListItem({
body: {
listId: get('listid').innerText,
item: {
name: text
}
},
onDone: function () {
var res = JSON.parse(this.response);
if (res.success) {
reload();
} else {
console.log(this.response);
}
}
});
}
});
} else {
[].forEach.call(document.querySelectorAll('.list_delete'), function (el) {
el.addEventListener('click', function (e) {
var li = e.target.parentElement,
listId = li.id.replace(/^list_/, '');
console.log(listId);
lc.delList({
listId: listId,
onDone: function () {
//console.log(this.response);
reload(true);
}
});
});
});
}
7.1.5 - landscape/layouts/create.ejs
1
2
3
4
5
6
<h2>Create new:</h2>
<div id="create">
List Name: <input class="meta" name="name" type="text" value="The foo list"><br>
The db folder is where the list database will be stored.
9 - Conclusion
This project was put together pretty quickly, but I just wanted a full stack example to write about for my collection of posts on express. I might work on this project a bit more to address some of it’s shortcomings. However so far it all ready works good enough as a way of maintaining a todo list. Looks like I might start using it in place of my old txt file solution, to say the least..