Keystonejs Succinctly
Keystonejs Succinctly
Keystonejs Succinctly
js Succinctly
By
Manikanta Panati
2
Copyright 2017 by Syncfusion, Inc.
If you obtained this book from any other source, please register and download a free copy from
www.syncfusion.com.
The authors and copyright holders provide absolutely no warranty for any information provided.
The authors and copyright holders shall not be liable for any claim, damages, or any other
liability arising from, out of, or in connection with the information in this book.
Please do not use this book if the listed terms are unacceptable.
3
Table of Contents
Summary ..............................................................................................................................13
Summary ..............................................................................................................................20
Defining models....................................................................................................................21
Summary ..............................................................................................................................36
4
Swig built-in filters ................................................................................................................40
Summary ..............................................................................................................................43
Summary ..............................................................................................................................54
Summary ..............................................................................................................................59
Summary ..............................................................................................................................69
Summary ..............................................................................................................................75
5
Expose endpoint for retrieving news .....................................................................................76
Summary ..............................................................................................................................83
6
The Story behind the Succinctly Series
of Books
Whenever platforms or tools are shipping out of Microsoft, which seems to be about every other
week these days, we have to educate ourselves, quickly.
While more information is becoming available on the Internet and more and more books are
being published, even on topics that are relatively new, one aspect that continues to inhibit us is
the inability to find concise technology overview books.
We are usually faced with two options: read several 500+ page books or scour the web for
relevant blog posts and other articles. Just as everyone else who has a job to do and customers
to serve, we find this quite frustrating.
We firmly believe, given the background knowledge such developers have, that most topics can
be translated into books that are between 50 and 100 pages.
This is exactly what we resolved to accomplish with the Succinctly series. Isnt everything
wonderful born out of a deep desire to change things for the better?
7
authors tireless work. You will find original content that is guaranteed to get you up and running
in about the time it takes to drink a few cups of coffee.
Free forever
Syncfusion will be working to produce books on several topics. The books will always be free.
Any updates we publish will also be free.
As a component vendor, our unique claim has always been that we offer deeper and broader
frameworks than anyone else on the market. Developer education greatly helps us market and
sell against competing vendors who promise to enable AJAX support with one click, or turn
the moon to cheese!
We sincerely hope you enjoy reading this book and that it helps you better understand the topic
of study. Thank you for reading.
8
About the Author
Manikanta Panati has spent the last 10 years perfecting enterprise-level application
development using Microsoft and open-source technologies. Recent projects include a very
popular coupon site in Asia and a Node.js-powered application that aggregates and maintains
business data for over 2.2 million businesses.
With Masters degrees in Information Systems and Project Management, and Microsoft Certified
Technical Specialist (MCTS) and Microsoft Certified Professional Developer (MCPD)
certifications, he still learns something new every day.
Born and brought up in beautiful Bangalore, India, and presently based out of equally beautiful
North Carolina, he works for a multinational financial institution managing the migration and
development of projects.
9
Chapter 1 Introduction
What is Keystone.js?
Keystone.js is a Node.js web framework for developing database-driven websites, applications,
and RESTful APIs. The framework is built on Express.js and MongoDB, and follows the Model-
View-Template design pattern. Express.js is the de facto web application server framework for
Node.js-based applications. MongoDB is a very popular NoSQL database. Keystone.js is free
and open source. The framework does a lot of heavy lifting and allows developers to focus on
clean and rapid development.
The framework provides an automatic administration interface that can be used to create, read,
update, and delete data in the application. The administration GUI is generated dynamically
through inspection of models and user options. Developers can use 20+ built-in field types that
provide the capability to manage data ranging from text, dates, geolocation, and HTML to
images and files uploaded to Amazon S3 or Microsoft Azure.
Installing Node.js
Node.js is an open-source, cross-platform runtime environment that is most commonly used for
developing JavaScript-based, server-side web applications. Node.js is becoming a very popular
tool of choice for building highly performant and scalable web applications due to its async
model of handling requests on a single thread. Node.js can be installed on a wide variety of
operating systems including Windows, Linux, and Mac OS. This book will primarily focus on
running Keystone.js on Node.js in a Windows environment.
To install Node.js:
1. Visit the download page on the Node.js official website.
2. Click on the download link for the latest stable release .MSI under Windows 32-bit or 64-
bit, depending on your machine architecture.
3. Once the download is complete, double-click on the installer file, which will launch the
Node.js installer. Proceed through each step of the installation wizard.
10
Figure 1: Node installer
At the Custom Setup screen during the installation, make sure that the wizard installs NPM
(Node Package Manager) and configures the PATH environment variable along with installing
the Node.js runtime. This should be enabled by default for all installations.
Once these steps have been completed, both Node and NPM should be installed on your
system.
11
Testing whether Node.js is installed properly
Run the following commands on a new command prompt window. You might need to open a
new instance of command prompt for the PATH variable changes to take effect. We should see
the versions output on the screen.
Installing MongoDB
MongoDB is an open-source, document-oriented database that is designed to be both scalable
and easy to work with. MongoDB stores data in JSON-like documents with dynamic schema
instead of storing data in tables and rows like a relational database (such as MySQL).
Tip: MLab offers a free managed sandbox for MongoDB that can be used as a test
ground instead of installing MongoDB locally.
C:\Program Files\MongoDB\Server\3.2\bin>mongod.exe
12
Figure 4: Start up the MongoDB server
Installing Yeoman
Yeoman is a set of tools for automating development workflow. It scaffolds out a new application
along with writing build configuration and pulling in build tasks and NPM dependencies needed
for the build. Keystone.js provides a very handy Yeoman generator to generate a new project.
Next, to install the Yeoman keystone app generator, use the following command.
This installs the generator as a global package, and can be used to generate new projects
without needing to reinstall the Keystone.js generator.
Summary
We have reached the end of the first chapter. Setting up a development environment is a very
important task, and we just did this. Weve covered the necessary requirements to begin
working with Keystone.js application framework. Onwards!
13
Chapter 2 Creating Your First Project
The Keystone.js framework has very few requirements. We need to make sure we have the
following installed:
Node Runtime
Node Package Manager
MongoDB Database
Yeoman package
Create a new project by issuing the following command at the command prompt within the
folder you intend to keep the project in (in this case, NodePress).
c:\nodepress> yo keystone
14
I specified the following information when creating the demo project:
Name: NodePress
Template: swig
Blog: no
Image Gallery: no
Contact Form: No
User: User
New Directory: no
Application configuration
Keystone.js uses an excellent Node.js library, namely dotenv, to load the configuration data at
runtime. In a fresh Keystone.js installation, the root directory of your application will contain a
.env file. This file can be used to hold all our configuration data. All of the variables listed in this
file will be loaded into the process.env global object when your application receives a request.
It is recommended that you do not commit this file to version control.
Each of the variables in the .env is declared as a key value pair separated by an equal sign.
Keys are generally written in upper case.
COOKIE_SECRET=oQQ*s0pz5(bF4gpmoNwM|BDB~db+qwQ`K>Ik~*R2D
MANDRILL_API_KEY=NY8RRKyv1Bure9bdP8-TOQ
15
To access the configuration variables in our application, we can use them as presented in Code
Listing 7.
MONGO_URI=mongodb://user:password@localhost:27017/databasename
16
Figure 6: First run of development server
This command will serve up your project on port 3000. At the first run, Keystone.js will attempt
to apply an updatea framework function, which will try to create the admin user that was
configured during the project scaffolding. Navigate to http://localhost:3000 and you should see
the Keystone.js landing page.
17
Figure 7: Application landing page
Start up our app using the node keystone.js command and open
http://127.0.0.1:3000/keystone/signin in your browser. You should see the administration login
page shown in Figure 8.
18
Figure 8: Administration login
Log in using the user credentials set up during the scaffolding step (for example,
user@keystonejs.com and "admin"). You will see the admin site index page, as shown in Figure
9.
Figure 9: Administration UI
19
The user model on the page is automatically created for us by Keystone.js. If you click on
Users, you will see the admin user created for us. You can edit the admin users email address
and password to suit your needs and use the new credentials to log in to the application next
time.
Summary
Now that we have set up our development environment and an empty project, we can move on
to implementing advanced application features in Keystone.js. Before we can do anything
visual, we need something to display. In the next chapter, you will be introduced to the
Keystone.js models.
20
Chapter 3 Data Modeling in Keystone.js
The Mongoose object document mapper (ODM) included with Keystone.js provides a beautiful,
simple API implementation for working with your MongoDB database. Each database collection
has a corresponding "model" that is used to interact with that collection. Models allow you to
query for data in your Mongo collections, as well as insert new documents into the collection.
Mongoose provides an abstract and common interface to the data in a document database. The
ODM makes it very easy to convert data between JavaScript objects and the underlying Mongo
documents.
Defining models
With Keystone.js, creating a model is as easy as defining a JavaScript file and specifying a
number of attributes assigned to that file. Let's start with a very basic model for our news
entries. Create a new file named News.js in the project's Models directory and enter the
following code.
/**
* News Model
* ==========
*/
News.add({
title: { type: String, required: false },
state: { type: Types.Select, options: 'draft, published, archived',
default: 'draft', index: true },
author: { type: Types.Relationship, ref: 'User', index: true },
publishedDate: { type: Types.Date, index: true, dependsOn: { state:
'published' } },
content: { type: Types.Html, wysiwyg: true, height: 400 }
});
21
News.register();
There is a lot going on, so let's start with the require statement and work our way down. We
begin by importing the standard Keystone library and obtaining a reference to the Keystone
field types.
Next is the News model definition. Our News model is an object that is an instance of the
keystone.List. By relying on the keystone.List, our News object will inherit a variety of
helpers that we'll use to query the database.
Before adding fields to the News model, we define the name of the model as the first parameter
to the listin our case, News. The second parameter is an object that can be used to assign
behaviors to the News model. The autokey option is used to generate slugs for the model,
which we will use to give our news entries some nice URLs. The URL is generated from the title
of the post and can be accessed via the slug property of a news post. If the unique option is set
to true, Keystone.js validates that no other post exists with the same title as the one being
entered. This is an easy way to prevent duplicate news.
Each post can have multiple fields within it, which can be used to enter relevant data. The
attributes of the News model are a simple mapping of the names and data that we wish to store
in the database. They are listed as follows:
Title: This field can hold a string and is used for storing the news post title. The
required option is useful to validate that the field has a value before it is saved. A
database index is also used to enforce this.
State: This is a field in which to save the status of the news post. We use a select field
type, so the value for this field can be set to one of the given choices. The default option
is set to a draft status.
Content: This is the field to be used to store the description of the ticket. The Text
area field type will display a text area within the admin UI.
Author: This field will hold a reference to the user who created the news post. The field
is like a foreign key that defines many-to-one relationships in a relational database. This
field is displayed as an autosuggest text box in the admin UI that allows us to pick a
single user. Setting the many option to false indicates that only a single user can be
selected. Setting the index option to true will tell Keystone.js that we are interested in a
database index to be created for this field. The categories field can be set up similarly
to a relationship field.
PublishedDate: This date-time field indicates when the news post was created by the
user. Since we are using the default value of Date.now, the date will be saved
automatically when creating a new post object.
The defaultColumns option allows you to set the fields of your model that you want to display
in the admin list page. By default, only the object ID is displayed. In Code Listing 10, we are
specifying the title, state, author, and publishedDate as the default columns to display in
the admin UI, with state, author, and publishedDate being given column widths. The call to
register on our keystone.js list finalizes the model with any attributes and options we set.
22
Restart the application and refresh the administration page. You should see the option to
manage news items, as shown in Figure 11.
Click News and add a new news item with the green Create News button. You will be provided
with an autogenerated UI with all the fields that were defined in the News model.
23
Figure 12: Manage news in administration UI
Timestamps
The track option in the list initialization options allows us to keep track of when and who
created and last updated an item.
/**
* News Model
* ==========
*/
var News = new keystone.List('News', {
autokey: { path: 'slug', from: 'title', unique: true },
/* Automatic change tracking */
track: true
});
24
These fields are automatically added:
An alternate way of registering the track functionality is to use the track property on the News
list.
News.track = true;
Collection names
Note that we did not tell Keystone.js which MongoDB collection to use for our News model. The
plural name of the model will be used as the collection name unless another name is explicitly
specified. So, in this case, Keystone.js will assume the News model stores documents in the
News collection. You may specify a custom collection by defining a schema property on your
model.
/**
* News Model
* ==========
*/
Primary keys
Keystone.js will assume that each document has a primary key column named _id that holds
the MongoDB object ID. This field is generally used for querying as well as looking up related
documents.
25
Adding relationships to model
Each model in an application can be related to another model in a couple of ways. They may be
connected under a one-to-many relationship, or a many-to-many relationship.
One-to-many relationships are used when one model document can be associated with multiple
documents of another single model. For instance, a user can author many news posts, and one
news post can belong only to one user.
/**
* News Model
* ==========
*/
News.add({
author: { type: Types.Relationship, ref: 'User', index: true, many:
false }
});
We have defined the relationship between a news post and a user on the News model. The field
is of type Types.Relationship and the ref option is set to the User model, which indicates
the model it is related to. Setting the many option to false indicates that we can only select one
user for this field.
Restart the application and add a few news items. The author column should be populated as
per the relationship.
26
Figure 13: Authors related to News
To represent the relationship from both sides, we can define the relationship on the user model
as well. We can do this by calling the relationship method on the user model. Add the below
line to the user model.
/**
* User Model
* ==========
*/
User.relationship({ path: 'news', ref: 'News', refPath: 'author' });
path: This option defines the path of the relationship reference on the model.
ref: This option is the key of the referred model (the one that has the relationship field).
refPath: This option specifies the field of the relationship being referred to in the referred
model.
Click on the admin user, and you should see the list of news articles authored by that user in the
Relationships section.
27
Figure 14: News related to a single author
String
Number
Date
Buffer
Boolean
Mixed
ObjectId
Array
28
These data types are sufficient to store raw data, but make the application very difficult to work
with as it grows. Keystone.js addresses this problem by wrapping the basic data types with
advanced functionality and calling them field types. There are quite a few field types available,
and they are very simple to use. We have already used a few of these when we defined the
News model earlier. The available field types are:
Text
Boolean
Code
Color
Date
Datetime
Email
Html
Key
Location
Markdown
Money
Name
Number
Password
Select
Text
Textarea
Url
AzureFile
CloudinaryImage
CloudinaryImages
Embedly
LocalFile
S3 File
Some field types include helpful underscore methods, which are available on the item at the
field's name preceded by an underscore. For example: use the format underscore method of the
publishedDate DateTime field of the News model like in Code Listing 16.
Virtual properties allow us to format the data in fields when retrieving them from a model or
setting their value. A virtual property is added to the underlying Mongoose schema. Let us add a
virtual property that returns the year in which a news post was published.
29
Code Listing 17: Define virtual property on News model
News.schema.virtual('publishedYear').get(function () {
return this._.publishedDate.format('YYYY')
});
The advantage of virtual properties is that they are not persisted to the document saved within
MongoDB, yet are available on the document retrieved as a result of the query. The virtual
property can be used similarly to a regularly defined property, as shown in Code Listing 18.
console.log(newsItem.publishedYear);
Virtual methods
Virtual methods are similar to virtual properties and are added to the schema of the list. These
methods can be invoked from the templates if necessary. A good example is a method that can
return a well-formed URI to a news item.
News.schema.methods.url = function () {
return '/newsdetail/' + this.slug;
};
Keystone.js lists leverage the underlying Mongoose pre and post middleware. These are
methods that are defined on the model and are automatically invoked before or after a certain
operation, by the framework. A common example would be the pre and post save hooks,
which are used to manipulate the data in the model before it is saved to the MongoDB
collection.
For example, in our News model, we might want to automatically set the publishedDate value
when the state is changed to published (but only if it hasn't already been set).
We might also want to add a method to check whether the post is published, rather than
checking the state field value directly.
News.schema.methods.isPublished = function () {
return this.state == 'published';
}
30
if (this.isModified('state') && this.isPublished() &&
!this.publishedDate) {
this.publishedDate = new Date();
}
next();
});
var q = keystone.list('News').model.find();
q.exec(function(err, results) {
var newsitems = results;
next(err);
});
To fetch a news item that matches the slug, we can use the findOne method shown in Code
Listing 22. The slug can be read from the req.params collection.
var q = keystone.list('News').model.findOne({'slug':req.params.slug})
q.exec(function(err, results) {
var newsitems = results;
next(err);
});
For optimal performance, it is always advised to construct queries that retrieve only the
necessary data.
var q = keystone.list('News').model
31
.findOne({'slug':req.params.slug})
.select('title status author');
q.exec(function(err, result) {
var newsitem = result;
next(err);
});
Counting results
To count the number of documents associated with a given query, use the count method.
var q = keystone.list('News').model.count();
q.exec(function(err, count) {
console.log('There are %d news items', count);
next(err);
});
Ordering results
The sort method can be used in conjunction with the find method to order results of a query.
The following example will retrieve all news, ordered by title.
var q = keystone.list('News').model.find().sort('title');
q.exec(function(err, results) {
var news = results;
next(err);
});
By default, the results are sorted in ascending order. This default behavior can be reversed by
prefixing a minus sign to the field that is being used to sort.
var q = keystone.list('News').model.sort('-title');
q.exec(function(err, results) {
var news = results;
next(err);
});
32
Filtering results
The where method can be used to conditionally find documents with attributes that we are
interested in. Multiple where clauses can also be chained together. In the following example, let
us try to retrieve news items that have status as published.
var q = keystone.list('News').model
.where('state').equals('published');
q.exec(function(err, results) {
var news = results;
next(err);
});
To retrieve a small subset of documents, for instance, the ten most recently added news items,
we can do so using the limit method.
var q = keystone.list('News').model
.limit(10);
q.exec(function(err, results) {
var news = results;
next(err);
});
If you wanted to retrieve a subset of documents beginning at a certain offset, you can combine
the limit method with the skip method. The following example will retrieve the ten most recent
news items beginning with the sixth record.
var q = keystone.list('News').model.skip(6)
.limit(10);
q.exec(function(err, results) {
var news = results;
next(err);
});
33
Test existence of a field
We can use the exists method to determine whether a particular document contains a field
without actually loading it. For example, to determine a list of news items that are empty (that is,
where the content field does not exist on the News MongoDB document), use the following
statements.
var q = keystone.list('News').model
.where('content')
.exists(false);
q.exec(function(err, results) {
var news = results; //list of news with missing content
next(err);
});
To create and save a new document, use the save method. Youll first create a new instance of
the desired model, update its attributes, and then execute the save method.
newItem.save(function (err) {
if (err) {
console.error("Error adding News to the database:");
console.error(err);
} else {
console.log("Added news + newItem.title + " to the database.");
}
done(err);
});
Upon saving, the new News items will have a unique slug generated based on the title because
the autokeyoption was set on the model.
34
Updating an existing document programmatically
var q = keystone.list('News').model.findOne({'slug':req.params.slug})
q.exec(function(err, item) {
if (err) return res.apiError('database error', err);
if (!item) return res.apiError('not found');
});
});
Let us assume we receive the slug of a news item via a form post along with the changes.
Code Listing 32 first retrieves a news item that matches the provided slug. If the item is found,
any matching fields and their values (from the form post) are set to the data object. The
getUpdateHandler method on the matching news item can process the updates to the
document via a call to the process method. The data object is provided as an input to this
method.
To delete a document, first locate the document, and then use the remove method.
35
Summary
In this chapter, we learned how to create and work with models to save, retrieve, and
manipulate data. These are the most basic operations in all web applications, and Keystone.js
makes it a breeze to implement.
36
Chapter 4 Templating with Swig
Swig is a simple, powerful, and extendable JavaScript template engine. A template engine is
typically used to display data that has been returned from a query to the database. The template
engine combines data and markup to generate an output that can be rendered by the browser.
Swigs syntax is very similar to many existing template engines such as Jinja2 and Django, used
by other programming languages. If you are familiar with any of these technologies, you should
find Swig really simple.
Swig templates can either end with a .swig or .html extension. You can control this in the
keystone.init method in the keystone.js file in the root of the application.
keystone.init({
//use .html extension for template files
'view engine': 'html',
});
{{ news.title }}
Swig follows the same rules as JavaScript. If a key includes non-alphanumeric characters, it
must be accessed using bracket-notation, not dot-notation.
We can also invoke functions and render the output as if we were rendering a variable. If you
recall, we introduced a virtual method named url in the previous chapter (see Code Listing 19).
37
We can invoke the url method from the template and render a URL to the detail page for the
news item.
To output comments, use a curly brace followed by the hash sign. Comments are removed by
the parser during rendering, and will not be seen even if you do a view-source on the rendered
HTML page.
{#
This is a comment.
#}
If statements
You may construct if statements using the if, elif, else, and endif directives.
{% if length(newsitems) > 10 %}
I have more than 10 records
{% elif length(newsitems) < 5 %}
I have less than 5 records!
{% else %}
I don't have any records!
{% endif %}
Boolean operators like and and or can be used within logic tags. Code Listing 39 is an example
illustrating the use of the and conditionals.
We can also use built-in JavaScript functions within the conditional statements.
38
Code Listing 40: Use JS functions in conditionals
{% if news.title.indexOf(critical) > -1 %}
This is a critical news item
{% endif %}
Loop statements:
Swig also supports looping statements to iterate over arrays and objects. To iterate over the
tags array in the news item object, add the following markup to the template.
<ul>
{% for tag in newsitem.tags %}
<li> {{tag}} </li>
{% endfor %}
</ul>
Swig has a collection of very helpful loop control helpers. These provide additional information
about the state of the loop in an iteration.
During every for loop iteration, the following helper variables are available:
<ul>
{% for tag in newsitem.tags | reverse %}
<li> {{tag}} </li>
{% endfor %}
</ul>
39
Swig built-in filters
Filters are methods through which output can be manipulated before rendering. Filters are
special functions that are applied after any object token in a variable block using the pipe
character (|). Filters can also be chained together, one after another.
If, for example, we wanted to convert the title of a news item to title case and strip any HTML
tags that might have been input, we can use the title and striptags filters.
<div>
News Title - {{newsItem.title | title | striptags}}
</div>
Template inheritance
Since most web applications maintain the same general layout across various pages, it's
convenient to define this layout as a single Swig template and use some kind of template
inheritance/injection to be able to render specific partial templates. Swig makes this easy with
extends and block directives.
40
Create a template named layout.swig and save it under the /templates/layouts folder with the
following content.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}NodePress{% endblock %}</title>
{% block head %}
<link rel="stylesheet" href="main.css">
{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
Next, to use the base template in another template, create a template named news.swig and
save it in the templates/views directory with the following content.
{% extends 'layout.html' %}
{% block content %}
<p>We will display the news details here</p>
{% endblock %}
Template partials
Templates can easily get bulky and difficult to maintain if we do not organize the contents in a
good manner. An easy way to organize the different sections of a template is to use template
partials. Template partials are pieces of templates that reside in separate files and are
combined together to make a single template.
For example, the preceding news layout template can leverage multiple partials that are
concerned with displaying the header and footer of the pages.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
41
<title>{% block title %}NodePress{% endblock %}</title>
{% block head %}
<link rel="stylesheet" href="main.css">
{% endblock %}
</head>
<footer>
© nodepress 2016. All rights reserved.
</footer>
</html>
{% include 'header.swig' %}
<body>
{% block content %}{% endblock %}
</body>
{% include 'footer.swig' %}
Another recommended scenario in which to use template partials would be within loops. The
code will be much easier to understand and maintain.
Macros
A macro is a function in Swig that returns a template or HTML string. This is used to avoid code
that is repeated over and over again and reduce it to one function call. For example, the
following is a macro to show a news item.
{% macro showNews(news) %}
<div class="post">
<h2>
42
<a href="{{ news.url() }}">{{ news.title }}</a>
</h2>
<p>Posted
{% if news.publishedDate %}
<br>on {{ news._.publishedDate.format("MMMM Do, YYYY") }}
{% endif %}
</p>
</div>
{% endmacro %}
Now, to quickly display a news item in any template, call your macro using the following.
{{ showNews(newsObj) }}
Summary
In this chapter, you learned about Swig, the powerful and flexible templating option for Node.js.
There are other similar frameworks, and I would suggest you play around with a few to find the
one that best fits your style of coding.
43
Chapter 5 Working with Views
Up until this point, we have seen how to retrieve data from the database (models) and how to
use templates to display the data on the browser. The missing piece is the view, which is the
link between the model and the templates. Views are responsible for handling data transmission
to and from the template and the database.
Views are JavaScript modules and reside in a folder at /routes/views within the application.
Code Listing 53 illustrates a simple route. The route defines that the inline function should be
executed as a result of receiving a request at the root/route in the application. The HTTP
request should be an HTTP GET. The executed function receives both the request and response
context objects as input. In the example, the string welcome to nodepress is output on the
screen as part of the response.
Keystone.js supports the following routing methods that correspond to HTTP methods:
get
post
put
head
delete
options
trace
copy
lock
mkcol
move
purge
propfind
proppatch
44
unlock
report
mkactivity
checkout
merge
m-search
notify
subscribe
unsubscribe
patch
search
connect
The most commonly used HTTP verbs are GET and POST. POST is used to send data to the
server from web forms. The following example illustrates the syntax for a simple POST route.
There is a special routing method, app.all(), which is not derived from any HTTP method but
can be used to respond to incoming requests on any HTTP verb.
app.get('/news', routes.views.newslist);
Next, define the newslist view in /routes/views/newslist.js with the following code.
45
Code Listing 57: List view
// Init locals
locals.section = 'news';
locals.data = {
news: []
};
keystone.list('News').model.find().sort('title').exec(function (err,
results) {
if (err || !results.length) {
return next(err);
}
locals.data.news = results;
next(err);
});
});
};
The view is an instance of the keystone.View object. The view queries the database for all
news posts in a sorted manner. The news items are stored in an array named data.news. At
the end of the view, we invoke the render method and pass in the name of the template. This
template will be combined with the data from the view and sent back to the browser to be
rendered.
{% extends "../layouts/default.swig" %}
46
{% block intro %}
<div class="container">
<h1>News List</h1>
</div>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-9">
{% if data.news.length %}
<div class="news">
<table class="table">
{% for news in data.news %}
<tr>
<td>{{news.title}}</td><td><a href='{{news.url()}}'>Read
News</a></td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
The template uses the Swig for loop to render the news posts. The rendered output will look
like that shown in Figure 15.
47
Figure 15: List of news
app.get('/newsdetail/:slug', routes.views.newsdetail);
Next, define the newsdetail view in /routes/views/newsdetail.js with the following code.
// Set locals
locals.section = 'news';
locals.filters = {
slug: req.params.slug
};
locals.data = {
48
news: ''
};
var q = keystone.list('News').model.findOne({
state: 'published',
slug: locals.filters.slug
}).populate('author');
});
};
Create a template named newsdetail.swig under the /templates/views folder with the
following content.
{% extends "../layouts/default.swig" %}
{% block intro %}
<div class="container">
<h1>{{data.news.title}}</h1>
</div>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-9">
{% if data.news.content %}
<div class="news">
{% autoescape false %} {{data.news.content}} {% endautoescape
%}
49
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
When the autoescape property is set to false, HTML will be displayed as it is rather than being
escaped as HTML entities. The output will look like Figure 16.
Because pagination links are a feature that is used throughout the application, let's create the
pagination included in our app's template directory.
50
Code Listing 62: Pagination links
{% if data.news.totalPages > 1 %}
<ul class="pagination">
{% if data.news.previous %}
<li>
<a href="?page={{ data.news.previous }}">
<span class="glyphicon glyphicon-chevron-left"></span>
</a>
</li>
{% else %}
<li class="disabled">
<a href="?page=1">
<span class="glyphicon glyphicon-chevron-left"></span>
</a>
</li>
{% endif %}
{% for p in data.news.pages %}
<li class="{% if data.news.currentPage == p %}active{% endif
%}">
<a href="?page={% if p == "..." %}{% if i %}{{
data.news.totalPages }}{% else %}1{% endif %}{% else %}{{ p }}{% endif
%}">{{ p }}</a>
</li>
{% endfor %}
{% if data.news.next %}
<li>
<a href="?page={{ data.news.next }}">
<span class="glyphicon glyphicon-chevron-right"></span>
</a>
</li>
{% else %}
<li class="disabled">
<a href="?page={{ data.news.totalPages }}">
<span class="glyphicon glyphicon-chevron-right"></span>
</a>
</li>
{% endif %}
</ul>
{% endif %}
Keystone.js provides a very easy API to fetch data from MongoDB in a paginated manner with
just a few lines of code. Lets update the newslist.js view to fetch news in a paginated object.
51
var view = new keystone.View(req, res);
var locals = res.locals;
// Set locals
locals.section = 'news';
locals.filters = {
slug: req.params.slug
};
locals.data = {
news: ''
};
var q = keystone.list('News').model.findOne({
state: 'published',
slug: locals.filters.slug
}).populate('author');
});
};
{% extends "../layouts/default.swig" %}
{% block intro %}
<div class="container">
<h1>News List</h1>
</div>
{% endblock %}
{% block content %}
<div class="container">
52
<div class="row">
<div class="col-sm-8 col-md-9">
{% if data.news.results.length %}
<div class="news">
<table class="table">
{% for news in data.news.results %}
<tr>
<td>{{news.title}}</td><td><a href='{{news.url()}}'>Read
News</a></td>
</tr>
{% endfor %}
</table>
</div>
{% include 'page_links.swig' %}
{% endif %}
</div>
</div>
</div>
{% endblock %}
Restart the application, and the pagination links should appear as shown in the following figure.
The Keystone.js pagination object returns a lot of useful metadata along with the results:
53
previous: Index of the previous page; false if at the first page.
next: Index of the next page, false if at the last page.
first: Index of the first result included.
last: Index of the last result included.
Since Keystone.js uses the express.static built-in middleware function in Express to serve
static assets, we reference assets as if they resided in the root of the application, as shown in
Code Listing 65.
Summary
In this chapter, we were able to understand the link among data models, views, templates, and
URLs in your application, including object pagination.
54
Chapter 6 Forms and Validation
Let us see how to use forms to modify the contents of nodepress directly. We will create forms
for working with the News model, learn how to receive and validate user data, and finally update
the values in the database.
{% extends "../layouts/default.swig" %}
{% block content %}
<div class="container">
<div class="panel panel-primary">
<!-- Default panel contents -->
<div class="panel-heading">Create News Item</div>
<div class="panel-body">
<form class="form-horizontal custom-form" action="/createnews"
method="post">
<div class="form-group">
<div class="col-sm-2 label-column">
<label for="name-input-field" class="control-
label">Title </label>
</div>
<div class="col-sm-6 input-column {% if
validationErrors.title %}has-error{% endif %}">
<input type="text" name="title"
placeholder="Title of the News" class="form-control"
value="{{form.title}}" />
</div>
</div>
<div class="form-group">
<div class="col-sm-2 label-column">
<label for="email-input-field" class="control-
label">Content </label>
</div>
<div class="col-sm-6 input-column" {% if
validationErrors.content %}has-error{% endif %}>
<textarea name="description"
55
placeholder="Describe the News" class="form-
control">{{form.content}}</textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-2 label-column">
<label for="" class="control-label">State
</label>
</div>
<div class="col-sm-6 input-column">
<select class="form-control" name="state">
<option value="draft" {% if form.state ==
'draft' %} selected{% endif %}>Draft</option>
<option value="published" {% if
form.state == 'published' %} selected{% endif %}>Published</option>
<option value="archived" {% if form.state
== 'archived' %} selected{% endif %}>Archived</option>
</select>
</div>
</div>
56
The form is pretty straightforward. We have HTML form fields for each of the model fields on the
News model.
As you see, we are taking advantage of the requireUser middleware to make sure that our
user is first logged into the application before being able to create a news item.
locals.form = req.body;
locals.data = {
users: []
};
});
57
var newNews = new News.model(),
data = req.body;
data.author = res.locals.user.id;
newNews.getUpdateHandler(req).process(data, {
flashErrors: true,
}, function (err) {
if (err) {
locals.validationErrors = err.errors;
} else {
req.flash('success', 'Your news item has been created!');
return res.redirect('/news/' + newNews.slug);
}
next();
});
});
};
The getUpdateHandler method will perform validation based on the model definition. We can
use the following snippet to highlight any fields that failed validation by applying the bootstrap
has-error CSS class in our template.
Flash errors are used to show an aggregate of all the errors that occur. The following figure
shows an example of flash errors.
58
Figure 19: Flash errors
To receive the form data on the server side, we use the request body on the HTTP post. The
getUpdateHandler method on an instance of the model can take in the post data and create a
new entry in the database. It is important that the object keys in the input data read from the
form post match up to the fields defined in the model.
Summary
In this chapter, we learned about the ease of integrating forms within a Keystone.js application.
Keystone.js form validation makes it very convenient to implement complex forms in any
application.
59
Chapter 7 Authenticating Users
Most dynamic web applications allow for some kind of user authentication and preference-
saving functionality. Let us look at how to allow users to create an account and log in and out
from our Keystone.js application.
There are a few configuration options that need to be set before using the session functionality.
These options should be set in the keystone.init() function within the keystone.js file. The
configuration options are:
session: Set this option to true if you want your application to support session
management.
auth: This option indicates whether to enable built-in authentication for keystones
Admin UI, or a custom function to use to authenticate users.
user model: This option indicates to Keystone.js which model will be used to maintain
the user information.
cookie secret: Use this option to specify the encryption key to use for your cookies.
session store: This identifies which session storage option to use (in-memory, mongo,
etc.).
After enabling the session-based authentication options, let us define routes that will be used for
authentication. Add the following routes to the route index file.
app.all('/join', routes.views.join);
app.all('/signin', routes.views.signin);
app.get('/signout', routes.views.signout);
60
Create an account
The first step in allowing users to create an account is to display the registration form. Create a
file named join.swig in the templates/views folder with the following content.
{% extends "../../layouts/default.swig" %}
{% block content %}
<div class="container">
<div class="panel panel-primary">
<div class="panel-heading">Create An Account</div>
<div class="panel-body">
<div class="col-md-6">
<form action="/join" method="post" class="form-horizontal">
<fieldset>
<div class="form-group required">
<label class="col-md-4 control-label">User Name*</label>
<div class="col-md-8">
<input class="form-control" id="username" placeholder="Pick a
user name" name="username" type="text" value="{{form.username}}">
</div>
</div>
<div class="form-group required">
<label class="col-md-4 control-label">First Name*</label>
<div class="col-md-8">
<input class="form-control" id="firstname" placeholder="First
name" name="firstname" type="text" value="{{form.firstname}}">
</div>
</div>
<div class="form-group required">
<label class="col-md-4 control-label">Last Name*</label>
<div class="col-md-8">
<input class="form-control" id="lastname" placeholder="Last
name" name="lastname" type="text" value="{{form.lastname}}">
</div>
</div>
<div class="form-group required">
<label class="col-md-4 control-label">Email Address*</label>
<div class="col-md-8">
<input class="form-control" id="email" placeholder="Email
address" name="email" type="email" value="{{form.email}}">
</div>
</div>
<div class="form-group required">
<label class="col-md-4 control-label">Password*</label>
<div class="col-md-8">
<input class="form-control" id="password" name="password"
61
placeholder="password" type="password">
</div>
</div>
<div class="form-group">
<label class="col-md-4 control-label"></label>
<div class="col-md-8">
<div style="clear:both"></div>
<button class="btn btn-primary"
type="submit">Join</button>
</div>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
Create the view named join.js under the /routes/views directory with the following code.
62
Code Listing 73: Registration view
if (req.user) {
return res.redirect('/');
}
locals.section = 'createaccount';
locals.form = req.body;
async.series([
function (cb) {
if (!req.body.username || !req.body.firstname ||
!req.body.lastname || !req.body.email || !req.body.password) {
req.flash('error', 'Please enter a username,
your name, email and password.');
return cb(true);
}
return cb();
},
function (cb) {
keystone.list('User').model.findOne({ username:
req.body.username }, function (err, user) {
if (err || user) {
req.flash('error', 'User already exists with that
Username.');
return cb(true);
}
return cb();
});
63
},
function (cb) {
keystone.list('User').model.findOne({ email:
req.body.email }, function (err, user) {
if (err || user) {
req.flash('error', 'User already exists
with that email address.');
return cb(true);
}
return cb();
});
},
function (cb) {
var userData = {
username: req.body.username,
name: {
first: req.body.firstname,
last: req.body.lastname,
},
email: req.body.email,
password: req.body.password
};
newUser.save(function (err) {
return cb(err);
});
], function (err) {
64
req.flash('error', 'There was a problem signing you up,
please try again.');
return next();
}
});
});
view.render(join');
The view uses the excellent async library that performs multiple operations in series. The first
(anonymous) function checks if the form inputs have been populated. The next method checks
if the username entered on the form already exists. If it exists, we return an error to the user.
The series operations terminate at this point. The next method checks if there is an existing
user with the same email address.
After all these operations have successfully completed, the user object is constructed and saved
to the database. On success, code to log in the user is called and the user is redirected to the
homepage.
To test the flash messages that render the error messages from failed form validation, submit
the form without filling any values. The error should appear as shown in the following figure.
65
Since we used the app.all method to define the route, both GET and POST are directed to a
single action URL. During a GET, the form is rendered, and during a POST, the form is validated.
{% extends "../layouts/default.swig" %}
{% block content %}
<div class="container">
<div class="panel panel-primary">
<!-- Default panel contents -->
<div class="panel-heading">Login to Nodepress</div>
<div class="panel-body">
<div class="col-md-4">
<form role="form" action="/signin" method="post">
<div class="form-group">
<label for="sender-email" class="control-label">Email
address:</label>
<div class="input-icon">
<input class="form-control email" id="signin-email"
placeholder="you@mail.com" name="email" type="email" value="">
</div>
</div>
<div class="form-group">
<label for="user-pass" class="control-label">Password:</label>
<div class="input-icon">
<input type="password" class="form-control"
placeholder="Password" name="password" id="password">
</div>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary " value="Login">
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
66
The markup for our login form is pretty straightforward. We have defined input fields for the
user's email address and password. The form will POST to the /signin URL. If there are errors
during user authentication such as invalid email or password, we display those errors using the
FlashMessages.renderMessages static method that is offered by Keystone.js. We have
included the following piece of code in our layout file /templates/layouts/Default.swig to render
the flash messages.
{{ FlashMessages.renderMessages(messages) }}
Create the view named signin.js under the /routes/views directory with the following code.
if (req.user) {
return res.redirect('/mytickets');
}
locals.section = 'signin';
view.on('post', function (next) {
if (!req.body.email || !req.body.password) {
req.flash('error', 'Please enter your email and password.');
return next();
}
});
67
view.render('signin');
In the view, we check whether the user has already logged in. If they have logged in, we redirect
the user to the homepage. If the user has not logged in and has submitted the login form, we
will process the login request. To validate the form contents, we check if the user has provided
an email address and a password. If either one is empty, we set a flash error indicating the
missing data and return the callback. If the user has provided valid credentials, then the function
will regenerate a new session and complete the sign-in process.
The rendered login form will look like the following figure.
Sign out
To sign a user out, we should call the keystone.session.signout method. The signout
operation will clear the user's cookies, set the request user object to null, and regenerate a new
session. Upon completion, the user will be redirected to the homepage.
Create a view named signout.js under the /routes/views directory with the following code.
68
Authentication middleware
Keystone.js has built-in middleware that can be leveraged as part of a request and response
cycle. This is especially useful to check if requests need to be blocked or allowed based on
whether the user has authenticated themselves.
app.all('/profile*', middleware.requireUser);
The preceding piece of code applies the requireUser method before a request reaches any
route that follows /profile.
The middleware code resides within the routes/middleware.js file. The requireUser method
is implemented as shown in the following snippet.
/**
Prevents people from accessing protected pages when they're not
signed in.
*/
if (!req.user) {
req.flash('error', 'Please sign in to access this page.');
res.redirect('/keystone/signin');
} else {
next();
}
};
Summary
In this chapter, we looked at how we can easily set up an authentication system with
Keystone.js. Features such as password recovery and reset can also be easily implemented.
Readers should also look at securing applications using cookies and cross-site request forgery
(CSRF) protection that Keystone.js facilitates.
69
Chapter 8 Administration Interface
The admin interface provided by Keystone.js is possibly the best reason for the popularity of
Keystone.js over other similar Node.js web frameworks. The admin interface does a lot of heavy
lifting for developers, with minimal configuration. It provides a fully featured and extremely
tailored content management system (CMS) for Create, Update, Delete operations on models.
In this chapter, lets learn how easy it is to configure and customize the admin interface to get
the functionality we desire.
To log in to the admin panel, use the credentials that were set during the creation of the project
using Yeoman. The default credentials are user@keystone.js for email and admin for
password. After logging in you should see something similar to this.
70
A few things to note in this screen. First, Keystone.js will by default add links to manage users to
the navigation menu. Second, any models that we save to the models folder will show up in the
admin area and are grouped under the OTHER label. Thus, the link to manage News shows up
under the label OTHER.
Let's try editing one of the news items that we have in our MongoDB database. Click on the
News link, and you should see a screen similar to the following.
Click on the green Create News button at the top, and fill in the form to add a new news item.
An autogenerated form is created, as shown in the following figure. The form is created by
inspecting the models and model options dynamically.
71
Figure 26: Add a news item
Just make sure to set the State field to Published for it to show up on the news page of the
site. The delete news button can be used to remove unwanted entries. The Author input field
automatically shows an autocomplete text box since it was marked as a relationship field type
during model definition in code.
Customizing menus
The menu items in the administration site can be easily configured in the keystone.js file. The
menu items are stored in an object in the configuration with nav as the key. Let's add the news
menu item to the menu.
keystone.set('nav', {
'users': 'users',
'manageNews': 'news'
});
The updated navigation menu will look as shown in the following figure.
72
Figure 27: Modified nav menu
The default columns option is a comma-delimited list of default columns to display in the Admin
UI list view. You can specify width in either pixels or percent after a | pipe character.
We can also select additional columns or remove columns from the list view using the Columns
drop-down menu on the right-hand side, as shown in the following figure.
Columns that are already defined are shown with a check next to them. The drop-down menu
allows us to choose additional columns or remove existing columns on the fly.
73
Search and filter
Most admin panels allow for users to search and filter content. Keystone.js provides powerful
search and filter options out of the box. The Search box provided in the admin panel performs a
search on the title of the news by default. We can specify additional columns that need to be
searched by setting the searchFields options on the model.
Figure 29 shows an added filter that allows us to search on the content as well as title. We have
options to search for an exact match or for content that contains our keywords. The invert option
is used to negate the search query.
Keystone.js provides intuitive and powerful filter options depending on the field type. For
example, in Figure 30, we have enabled the filter for the published date, and Keystone.js
automatically shows options to filter by date on, after, before, and between two dates.
74
Summary
We have covered the Keystone.js admin interface, which lets us handle routine Create, Read,
Update, and Delete operations almost for free. We have a powerful and friendly way to create
test data, and one that would serve us well for production purposes if we wanted.
75
Chapter 9 Building REST APIs
REST APIs expose interfaces that allow various clients to read and write data in a consistent
manner. REST APIs are resource-centric, which means that the methods are only concerned
with the underlying resource and will not respond to arbitrary service methods. REST API
methods work by mapping HTTP verbs into API calls. The most common HTTP verbs used with
REST APIs are GET, POST, PUT, and DELETE. There may be many ways of implementing
RESTful APIs, and each developer tends to follow their own conventions as to what RESTful
means to them. However, the idea should be to keep the API aligned with the application
architecture and design.
The keystone.middleware.api parameter in the route adds the following shortcut methods for
JSON API responses:
res.apiResponse (data)
res.apiError (key, err, msg, code)
res.apiNotFound (err, msg)
res.apiNotAllowed (err, msg)
The apiReponse method returns the response data in the JSON format. It can also
automatically return data in JSONP if there is a callback specified in the request parameters.
76
The apiError method is a handy utility to return error messages to the client from our APIs. It
returns an object with two keyserror and detailwhich contain the exception that occurred. By
default, it returns an HTTP status of 500 if the code parameter is not passed. The apiNotFound
method provides a quick way to raise a 404 (not found) exception from our APIs. The
apiNotAllowed method provides a quick way to raise a 403 (not allowed) HTTP response from
our APIs.
Next, update the keystone.js file at the root of the application to include the api folder.
Next, add a new JavaScript file to the routes/api folder and name it news.js with the following
code.
/**
* Get List of News
*/
exports.getNews = function(req, res) {
News.model.find(function(err, items) {
res.apiResponse({
news: items
});
});
}
/**
* Get News by ID
*/
exports.getNewsById = function(req, res) {
News.model.findById(req.params.id).exec(function(err, item) {
77
res.apiResponse({
news: item
});
});
}
To test the API, restart the node application and navigate to http://localhost:3000/api/news using
Chrome or the Postman add-on tool for the Chrome browser. We should see a list of news
returned to in JSON format.
78
Figure 32: API GetNewsById response JSON
Lets take a look at endpoint code that accepts a POST request, inserts a news item into our
collection, and returns the new document JSON. Update the routes file and include the following
route.
app.post('/api/news', keystone.middleware.api,
routes.api.news.createNews);
Next, add the following code to the news.js file under the api folder.
/**
* Create a News Item
*/
exports.createNews = function (req, res) {
var item = new News.model(),
data = req.body;
79
res.apiResponse({
news: item
});
});
}
To test the create functionality, use the Postman tool for Chrome and craft a POST request to
http://localhost:3000/api/news.
The getUpdateHandler method is the heart of this method. This method validates various
criteria that have been specified during model definition. For example, we have specified that
the title field is required. This will be properly validated automatically without the developer
needing to check for such constraints programmatically at every instance.
To check the validation in action, perform an invalid POST to the /api/news endpoint. In the
following example, we have specified an incorrect state for the news (pushed instead of
published). The returned response clearly mentions the reason for failure along with an
appropriate HTTP error code (500).
80
Figure 34: Testing the getUpdateHandler method
Now, issue a GET request to http://localhost:3000/api/news, and we should see the newly
created news post returned.
81
Expose endpoint for deleting news
The delete operation is pretty straightforward. We should expose an endpoint that responds to
a call with an HTTP DELETE verb along with the ID of the news that needs to be removed. Add
the following route to the application.
app.delete('/api/news/:id', keystone.middleware.api,
routes.api.news.deleteNewsById);
/**
* Delete a News Item
*/
exports.deleteNewsById = function (req, res) {
News.model.findById(req.params.id).exec(function (err, item) {
item.remove(function (err) {
if (err) return res.apiError('database error', err);
return res.apiResponse({
success: true
});
});
});
}
The code uses the findById method to retrieve the document we intend to delete. If we do not
find the document or encounter exceptions while removing the document, then we return an
error to the client. If we do find the document, we remove it and return an object indicating the
success status.
82
Figure 36: Delete news item
Summary
In this chapter we saw how extremely simple it is to expose REST endpoints in a Keystone.js
application. The endpoints do not enforce strict role validation and authentication rules and may
not be appropriate for production environments as is. However, those rules can be easily added
to the Keystone.js middleware.
83