Home | Documentation | Core | CLI | Install |
co-koa-core module status
co-koa-cli module status
co-koa-mongoose-plugin module status
An opinionated MVC that let’s you choose what should/shouldn’t be a Model; inspired by Grails MVC, but viewed through the lens of Koa. Just like Koa, Co.Koa is about pragmatism; it encourages developers to interface with the underlying koa.app via Plugins. Mixin different databases and endpoints, it’s up to you!
When installed via the CLI, Co.Koa ships with support for MongoDB (via mongoose) and a layout that comfortably interfaces with VueJS or Handlebars (HBS); but, hey, don’t let that stop you choosing other toolchains!
Co.Koa obeys convention over configuration. One of Co.Koa’s greatest strength comes in its implementation of Dependency Management. Controllers, Models and Services are each supplied with a powerful callback that reads and feels like a JQuery call. No need to worry about requiring reams of files from across your project.
Structure & Installation
To install Co.Koa please visit: the wiki installation page
Co.Koa’s directory structure is as below
\api\controllers
\models
\services
\views\helpers
\layouts
\partials
\config
\i18n
\node_modules
at the root of the project is an app.js
file that loads in the co-koa-core
module and configures it accordingly.
Dependency Management
Nearly every component in the Co.Koa toolchain is supplied a Dependency Management tool (signified by a $
). The Dependency Management tool allows you to - among other things - easily load in Models, Services and Controllers.
Controllers, Models and their associated Services are all designed around the following boilerplate:
module.exports = function ($) {
return {};
};
Example
Suppose we are using Co.Koa’s mongoose functionality OOB; and we have a Book
document (table) in our Mongo database, modelled within our Co.Koa project. Furthermore, let’s suppose we’ve setup a BookService
that supports our controllers’ interactions with the Book
model. To expose the Book
mongoose model within our Bookservice
we simply do as below:
module.exports = function BookService ($) {
const Book = $('Book');
return {
const book = new Book({ ... }).save();
...
};
};
Perhaps a BookController
model would like access to the BookService
; simple!
module.exports = function BookController ($) {
const bookService = $('BookService');
return {
...
};
};
not a require
or import
statement in sight! Everything is routed for you under the hood thanks to Dependency Management!
As of Co.Koa@1.8.0 the Co.Koa core now supports custom Dependencies via a Dependency Register
Models
Co.Koa models are a pragmatic concept. By default, your build will come installed with a mongoose plugin that is simply a light-touch abstraction of the mongoose API; featuring all the components one would expect to find therein. However, you can easily switch mongoose off and roll out your own persistence mechanisms with Co.Koa’s plugin functionality (using local Databases and ORMs, serverless configurations or whatever!)
For the OOB Mongoose approach, you’re dealing with the same mongoose API you already know, only you’re not having to worry about requirements and which object goes where! Thus, you need only defer to mongoose API’s own documentation to know what a component does!
Continuing our example from above let’s contrive simple Book
and Author
models.
Book
module.exports = function Book ($) {
const Author = $('Author'); // load the Author mongoose model, used in the public method defined below.
return {
_modelType: 'mongoose', // tells Co.Koa that this model uses the mongoose model plugin. remove this flag to simply return this object.
schema: { // represents the mongoose schema like-for-like
Accredited: {
authors: [{
type: 'ForeignKey', // also supprots the keywords 'FK' or 'ObjectId'. Co.Koa will replace these 3 with the correct reference ObjectId on load!
ref: 'Author'
}]
},
title: {
type: String,
trim: true,
default: '(Unnamed Book)'
}
},
/* ------ OPTIONAL COMPONENTS ----- */
index: { // mongoose secondary indexes
title: 1,
Accredited: -1
},
methods: { // public methods
async findAssociatedAuthor () {
const author = await Author.findById(this._doc.Accredited.authors[0]);
return author;
}
},
options: { runSettersOnQuery: true }, // mongoose options
statics: { // Static methods
async findCompleteReferenceByTitle (title) {
const book = await this.findOne({ title });
const author = await book.findAssociatedAuthor();
return Object.assign({}, book._doc, { Accredited: { authors: [author._doc] } });
}
}
};
};
observe that Co.Koa fully supports (and strongly encourages) await/async
throughout! No more callback hell! No more hefty promise statements! Just easy-to-read and powerful code!
Author
module.exports = function Author ($) {
return {
_modelType: 'mongoose',
schema: {
firstName: String,
lastName: String
},
/* ------ OPTIONAL COMPONENTS ----- */
virtuals: { // virtuals behave exactly as they would if supplied to a mongoose schema
fullName: {
get () { return `${this.lastName}, ${this.firstName}`; },
set (name) {
const fullName = name.split(' ');
this.firstName = fullName[0];
this.lastName = fullName[1];
return this;
}
}
}
};
};
Controllers
Controllers are also a piece of cake. Under the hood, your controllers are routed by koa-router (https://www.npmjs.com/package/koa-router).
Continuing with our Bookish theme, let’s look at a contrived book example:
'use strict';
module.exports = function BookController ($) {
const Book = $('Book');
const bookService = $('BookService');
return {
'GET /:id': async (ctx) => { // uri = Book/:id (':id' is variable)
const book = await Book.findById(ctx.params.id); // ctx.params.id contains the ":id" variable
ctx.body = book;
},
'GET /Author': async (ctx) => { // uri = Book/Author
const book = await Book.find({ title: 'Harry Potter and the Philosopher\'s Stone' });
const author = await book[0].findAssociatedAuthor();
ctx.body = author;
},
'GET /HarryPotter': async (ctx) => { // Book/HarryPotter
const harryPotter = await Book.findCompleteReferenceByTitle('Harry Potter and the Philosopher\'s Stone');
ctx.body = harryPotter;
}
};
};
Services
Services allow you to decouple your business logic from your application’s logic. Ideally, we want to keep controller logic svelte; and some code just doesn’t sit right in a model method. As we saw in the BookController
example above Co.Koa exposes services via the $
DependencyManager. Let’s continue the book theme by contriving BookService.js
in api\services
:
module.exports = function BookService ($) {
const Author = $('Author');
return {
async createAuthor () {
const author = await new Author({
fullName: 'J.K Rowling' }).save();
return author;
}
};
};
Co.Koa will sort out the requirements for you! The service is exposed via the $
DependencyManager in your controllers, models and other services as $('BookService')
.
Vue Integration
Co.koa now ships with @koa/cors
and will very happily interface with a vue instance in tow. With the release of co-koa-cli@1.0.0 VueJS is easy to ship as your development front end! For more information please visit the CoKoa Documentation
Views
Co.Koa Supports the handlebars .hbs extension using “koa-hbs-renderer” (https://www.npmjs.com/package/koa-hbs-renderer). supply your views, helpers, layouts and partials within the directories indicated below:
\api\views\helpers
\layouts
\partials
rendering .hbs is simple and powerful; suppose we have a view called SampleView.hbs
saved in the \api\views
directory. The view is expecting a single variable called action
to be passed to it:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en"><head></head>
<body> <p>I'm a view, I was called by the action: {{action}}</p> </body>
</html>
If we add the following to BookController
, we’re good to go!
'GET /HBSDemo': async (ctx) => {
await ctx.render('SampleView', { action: '/HBSDemo' });
}
you can add custom helpers with ease! for example, maybe you want to make hbs files handle more complex conditional statements. No prob, suppose we have a file saved at \api\views\helpers\CK.js
:
module.exports = {
eq: (foo, bar) => foo === bar,
ne: (foo, bar) => foo !== bar,
lt: (foo, bar) => foo < bar,
gt: (foo, bar) => foo > bar,
lte: (foo, bar) => foo <= bar,
gte: (foo, bar) => foo >= bar,
and: (foo, bar) => foo && bar,
or: (foo, bar) => foo || bar
};
now your .hbs file can use custom logic!
<ul>
<li>
{{#if (CK_and (CK_eq parent 'PartialSample')
(CK_ne parent 'SampleView')) }}
I am the product of an "if" condition using embedded operands!
{{else}}
I am the product of an "else" condition
{{/if}}
</li>
</ul>
Note that your helpers are prefixed with CK_
. Co.Koa uses koa-hbs-renderer
. For more information please navigate to the koa-hbs-renderer github. For more information on how to use Handlebars, please visit: http://handlebarsjs.com/
Testing
as of co-koa-cli
@^1.17.0, Co.Koa has baked in support for unit and integration testing with Jest thanks to the app.test.harness
.
const testHarness = require('../app.test.harness.js');
const coKoa = testHarness.init({ port: 3004, type: 'integration' });
describe('Integration Test Suite For Example', async () => {
const $ = coKoa.$
const Example = $('Example');
test('An integration test', async () => {
try {
const result = await new Example({ }).save();
expect(typeof result).toBe('object');
} catch (e) {
console.error(e.message)
}
});
afterAll(async (done) => {
await coKoa.app.close() // close the Koa app listener
await testHarness.destroy(done) // (optional) empty test database
});
});
for more information please see the testing documentation.