Koa Sample App (handlebars templating + RESTful API using MySQL, on Node.js)
This is the result of a self-learning exercise on how to put together a complete Node.js MySQL-driven Koa app.
When I started with Node.js (using Express), I found plenty of tutorials & examples on individual elements, but found it hard to stitch everything together; this was even more true with Koa. Being new to Node / Express / Koa, I found that understanding came very much by assembling all the different bits together.
While the Koa βhello worldβ certainly doesnβt flatter to deceive, thereβs obviously a long way to go after it. This does some of that. It is a template not a mini-tutorial. It puts a lot of the components of a complete system together: neither mini nor tutorial! (For a slightly less minimalist hello world than Koaβs, my koa-hello-world includes routing & templates in 17 lines of JS).
Having worked it all out, this is now largely a βnote-to-selfβ aide-memoire, but I hope it might help others in a similar situation. No one else will be doing all the same things, but seeing components operating together may be helpful.
It is of course simplistic, but unlike many tutorials, it assembles together many of the components of a complete system: in this case, basic interactive tools for viewing, adding, editing, and deleting (CRUD), with JWT-based authentication/login, and a matching RESTful API to do the same. Many systems may not require an API, but the API app can be used for RESTful ajax functions (illustrated in the edit member/team pages). Of course, real systems do much more, but often build on these core functions.
The database includes a couple of tables with related data and referential integrity β one step beyond where most tutorials go. Hence code is included for handling basic validation and referential integrity errors returned from the database.
Otherwise Iβve stripped it down to essentials. Thereβs no pretty styling! Thereβs no great UI. Just the bare building-blocks.
There are also sample integration/acceptance tests using mocha / supertest / chai.
I donβt have time to put together a full tutorial, but Iβve tried to make everything clear & well structured, and Iβve liberally commented the code.
Design choices
While itβs still rather leading-edge, Koa makes development far simpler than classic callback-style Node.js with Express. JavaScript used to be looked down on (misunderstood?) in some quarters, but it is gaining understanding as a mature and valid language (the benefits of prototypal inheritance are starting to be appreciated, rather than seen as βclassical inheritance got wrongβ). Itβs certainly vastly better to work with than PHP :)
The app is a classic web application, not a single-page app (βSPAβ).
It is built with a modular approach. There are three (composed) sub-apps: the bare bones of a public site, a web-based password-protected admin system using handlebars-templated html pages, and a REST API. Each of these is structured in a modular fashion; mostly each admin module has JavaScript handlers to handle GET and POST requests, and a set of handlebars templates; each API module has JavaScript handlers for GET, POST, PATCH, DELETE requests.
The highly-structured applications I work on often require ACID SQL databases with referential integrity, so MongoDB was out for me. MySQL and PostgreSQL should be pretty similar, but PostgreSQL is not yet so well supported for Koa.
Sometimes both SQL and object-oriented databases such as MongoDB are appropriate within the same app. To illustrate how simple this is, I have used MongoDB for access/error logging. And if you have no need for SQL, this sample app is also good boiler-plate for purely MongoDB-based app: just remove the MySQL elements.
For some people, a full JavaScript framework will work better. If youβre happy to plan out your own preferred structure, designing your own patterns means one less component to learn / conform to.
Thereβs always things others would do differently. If you have better ways of doing things, it will either be down to my preferences, or my ignorance! If you notice anything Iβve got wrong or have real improvements, let me know.
Admin app
The admin app just does basic adding/editing/deleting. Any real app will do much more, but will generally include and build on these basics.
I find Handlebars offers a good minimal-logic templates (mustache is too limiting, but I like to work with HTML).
The main app-admin.js sets up the database connection, handlebars templating, JWT authentication, 4xx/500 handling, etc (JWT authentication is held in (signed) cookies, for stateless sessions).
Iβve divided the app into routes, matching handlers/controllers, and a set of templates. The handlers have one function for each method/route, and either render a view, redirect (e.g. after POST), or throw an error.
API
The API returns JSON or XML (or plain text) according to the Accepts request header.
The main app-api.js sets up the database connection, content negotiation, JWT authentication, 4xx/500 handling, etc (JWT authentication is supplied in Bearer Authorization HTTP headers).
Routes are grouped into members, teams, team membership, and authentication. All but the simplest of these then go on to call related handlers.
The members.js and teams.js then handle the API requests. I use PATCH in preference to PUT so that a subset of entity fields can be supplied (correctly, a PUT will set unsupplied fields to null); otherwise everything is very straightforward REST API, hopefully all following best practice.
Special provision is made for boolean values, which are typically stored in MySQL as BIT(1) or TINYINT(1). In the admin section, where fields are handled individually, normal JavaScript type conversion rules take care of conversions between boolean and numeric values, but for the API boolean values have to be explicitly converted between 1/0 and true/false (for both JSON and XML).
Models
Models sit above the sub-apps, as they are used by both the admin app and the API.
I use light-weight models just to manage all operations which modify stored data in the database; individual handlers are responsible for obtaining data they require to render their templates (using SQL queries).
Dependencies
While very basic, this sample app incorporates together many of the components of a real application; as well as handlebars templates, MySQL, & JWT-managed logins, thereβs static file serving, body-parser for post data, lusca security headers, compression, logging, flash messages, etc, and mocha/chai for testing (Iβve ignored i18n which would introduce considerable complexity). Full details of course in package.json.
For Ajax, I use native JavaScript DOM API / fetch API rather than jQuery (e.g. document.querySelector()
).
The app uses the database set out below, with connection details as per .env
.
Demo
There is a running version of the app at koa-sample-app.movable-type.co.uk.
Local copy
If you would like to make a local copy to experiment with, you of course need Node.js and Git
installed, and also MySQL (with connection details as per .env
).
You will need admin.
, api.
, and www.
subdomains available; to do this, add a line such as
127.0.0.1 www.localhost api.localhost admin.localhost
to /etc/hosts
(on Unix/Mac), or
\Windows\System32\drivers\etc\hosts
(on Windows).
In MySQL, run the database schema script to create the database, and the test data script to populate it.
Then at the Unix command line, or using Git Bash on Windows:
$ git clone https://github.com/chrisveness/koa-sample-web-app-api-mysql.git
$ cd koa-sample-web-app-api-mysql
$ npm install
$ npm start
Then open a browser and go to http://www.localhost:3000
to run the app.
File structure
.
βββ app-admin
β βββ handlers
β β βββ ajax.js
β β βββ dev.js
β β βββ login.js
β β βββ members.js
β β βββ password-reset.js
β β βββ teams.js
β βββ routes
β β βββ ajax-routes.js
β β βββ dev-routes.js
β β βββ index-routes.js
β β βββ login-routes.js
β β βββ logs-routes.js
β β βββ members-routes.js
β β βββ password-routes.js
β β βββ teams-routes.js
β βββ templates
β β βββ partials
β β β βββ errpartial.html
β β β βββ navpartial.html
β β βββ 400-bad-request.html
β β βββ 404-not-found.html
β β βββ 500-internal-server-error.html
β β βββ dev-logs-access.html
β β βββ dev-logs-error.html
β β βββ index.html
β β βββ login.html
β β βββ logs.html
β β βββ members-add.html
β β βββ members-delete.html
β β βββ members-edit.html
β β βββ members-list.html
β β βββ members-view.html
β β βββ password-reset.email.html
β β βββ password-reset.html
β β βββ password-reset-confirm.html
β β βββ password-reset-request.html
β β βββ password-reset-reqeust-confirm.html
β β βββ teams-add.html
β β βββ teams-delete.html
β β βββ teams-edit.html
β β βββ teams-list.html
β β βββ teams-view.html
β βββ app-admin.js
βββ app-api
β βββ app-api.js
β βββ auth.js
β βββ cast-boolean.js
β βββ members.js
β βββ routes-auth.js
β βββ routes-members.js
β βββ routes-root.js
β βββ routes-team-members.js
β βββ routes-teams.js
β βββ team-members.js
β βββ teams.js
βββ app-www
β βββ templates
β β βββ 404-not-found.html
β β βββ 500-internal-server-error.html
β β βββ contact.html
β β βββ index.html
β β βββ navpartial.html
β βββ app-www.js
β βββ handlers-www.js
β βββ routes-www.js
βββ lib
β βββ handlebars-helpers.js
β βββ ip.js
β βββ log.js
β βββ mail.js
β βββ mongodb.js
β βββ mysqldb.js
β βββ pwned.js
β βββ ssl-middleware.js
β βββ validation-errors.js
βββ models
β βββ member.js
β βββ modelerror.js
β βββ model-super.js
β βββ team.js
β βββ team-member.js
β βββ user.js
βββ public
β βββ css
β β βββ admin.css
β β βββ base.css
β β βββ www.css
β βββ js
β β βββ .eslintrc.json
β β βββ dev-logs.js
β β βββ members-edit.js
β β βββ password-reset.js
β β βββ teams-edit.js
β βββ favicon.ico
β βββ robots.txt
βββ test
β βββ frontend
β β βββ basic-test.js
β βββ integration
β β βββ app-admin-tests.js
β β βββ app-api-tests.js
β βββ unit
β βββ model-member-tests.js
β βββ validation-error-tests.js
ββ .env
ββ app.js
ββ LICENSE
ββ package.json
ββ README.md
I originally structured this in a modular fashion as suggested by TJ, but Iβve since found it more convenient to work with a flatter structure (heresy!) as I found it unproductive to be constantly expanding and contracting folders. Go with what works for you.
Simpler applications can use a much flatter structure still, at the limit with no sub-folders at all; more complex ones might use a deeper structure. βHorses for coursesβ, as they say.
.env
Sensitive information such as database login credentials, SMTP parameters, etc, are held in environment variables. For local development, these can be kept in a .env file (which is .gitignoreβd, not checked in to a repository). For production, they will be set as environment variables and not stored in the file system.
A .env file for this project might look something like:
DB_MYSQL_CONNECTION = Host=eu-cdbr-west-01.cleardb.com; User=abcdef12345678; Password=abcdef12; Database=heroku_abcdef012345678
DB_MONGO_CONNECTION = mongodb://heroku_abcdefgh:[email protected]:12345/heroku_abcdefgh
SMTP_CONNECTION = service=gmail; [email protected]; auth.pass=mypassword
Database schema
-- Schema for βkoa-sample-web-app-api-mysqlβ app
create database `koa-sample-sandbox`;
use `koa-sample-sandbox`;
create table Member (
MemberId integer unsigned not null auto_increment,
Firstname text,
Lastname text,
Email text not null,
Active bit(1),
primary key (MemberId),
unique key Email (Email(24))
) engine=InnoDB charset=utf8 auto_increment=100001;
create table Team (
TeamId integer unsigned not null auto_increment,
Name text not null,
primary key (TeamId)
) engine=InnoDB charset=utf8 auto_increment=100001;
create table TeamMember (
TeamMemberId integer unsigned not null auto_increment,
MemberId integer unsigned not null,
TeamId integer unsigned not null,
JoinedOn date not null,
primary key (TeamMemberId),
key MemberId (MemberId),
key TeamId (TeamId),
unique key TeamMember (MemberId,TeamId),
constraint Fk_Team_TeamMember foreign key (TeamId) references Team (TeamId),
constraint Fk_Member_TeamMember foreign key (MemberId) references Member (MemberId)
) engine=InnoDB charset=utf8 auto_increment=100001;
create table User (
UserId integer unsigned not null auto_increment,
Firstname text,
Lastname text,
Email text not null,
Password text,
PasswordResetRequest text,
Role text,
primary key (UserId),
unique key Email (Email(24))
) engine=InnoDB charset=utf8 auto_increment=100001;
Test data
-- Test data for βkoa-sample-web-app-api-mysqlβ app
INSERT INTO Member VALUES
(100001,'Juan Manuel','Fangio','[email protected]', false),
(100002,'Ayrton','Senna','[email protected]', false),
(100003,'Michael','Schumacher','[email protected]', false),
(100004,'Lewis','Hamilton','[email protected]', true);
INSERT INTO Team VALUES
(100001,'Ferrari'),
(100002,'Mercedes'),
(100003,'McLaren');
INSERT INTO TeamMember VALUES
(100001,100001,100001,'1956-01-22'),
(100002,100001,100002,'1954-01-17'),
(100003,100002,100003,'1988-04-03'),
(100004,100003,100001,'1996-03-10'),
(100005,100003,100002,'2010-03-14'),
(100006,100004,100002,'2007-03-18'),
(100007,100004,100003,'2013-03-17');
INSERT INTO User VALUES
(100001,'Guest','User','[email protected]','c2NyeXB0AA8AAAAIAAAAAadRWAxJ7PVQ8T6zW7orsuCiHr38TPYJ9TGVbHEK5hvdbC7lCKxKdebdo0T0wR9Aiye4GQDHbLkcBNVVQZpBDtWGfezCWZvtcw4JZ90HDuhb',null,'guest'),
(100002,'Admin','User','[email protected]','c2NyeXB0AA4AAAAIAAAAAfvrpUA5jkh3ObPPUPNQEjbkHXk4vj4xPWH6N8yLEvbgkKqW5zqv3AgsHtTcSL2lzfviyMkXjybHPXeqDY62ZxHEvmTgEY6THddbqOUAOzTQ',null,'admin');
The full sample app is around 1,000 lines of JavaScript.