Skip to content

Instantly share code, notes, and snippets.

@JedWatson
Last active July 26, 2021 11:29
Show Gist options
  • Save JedWatson/9741171 to your computer and use it in GitHub Desktop.
Save JedWatson/9741171 to your computer and use it in GitHub Desktop.
Example of how to scaffold API endpoints for Posts in a Keystone project (based on the yo keystone example).

This is an example of how to scaffold API endpoints to list / get / create / update / delete Posts in a Keystone website.

It's a modification of the default project created with the yo keystone generator (see https://github.com/JedWatson/generator-keystone)

Gists don't let you specify full paths, so in the project structure the files would be:

routes-index.js        -->    /routes/index.js         // modified to add the api endpoints
routes-api-posts.js    -->    /routes/api/posts.js     // new file containing the Post API route controllers

It creates JSON endpoints for:

  • /api/post/list - lists all posts
  • /api/post/create - creates a new post
  • /api/post/{id} - returns the details of posts by id
  • /api/post/{id}/update - updates a post by id and returns the details
  • /api/post/{id}/delete - deletes a post by id

The create and update routes accept either GET or POST requests for simplicity, and look in either the URL parameters of the request body for data using the same paths as set on the models.

You can add your own logic in for security, default values, limiting fields etc. by configuring the functions exported by /routes/api/posts.js

var async = require('async'),
keystone = require('keystone');
var Post = keystone.list('Post');
/**
* List Posts
*/
exports.list = function(req, res) {
Post.model.find(function(err, items) {
if (err) return res.apiError('database error', err);
res.apiResponse({
posts: items
});
});
}
/**
* Get Post by ID
*/
exports.get = function(req, res) {
Post.model.findById(req.params.id).exec(function(err, item) {
if (err) return res.apiError('database error', err);
if (!item) return res.apiError('not found');
res.apiResponse({
post: item
});
});
}
/**
* Create a Post
*/
exports.create = function(req, res) {
var item = new Post.model(),
data = (req.method == 'POST') ? req.body : req.query;
item.getUpdateHandler(req).process(data, function(err) {
if (err) return res.apiError('error', err);
res.apiResponse({
post: item
});
});
}
/**
* Get Post by ID
*/
exports.update = function(req, res) {
Post.model.findById(req.params.id).exec(function(err, item) {
if (err) return res.apiError('database error', err);
if (!item) return res.apiError('not found');
var data = (req.method == 'POST') ? req.body : req.query;
item.getUpdateHandler(req).process(data, function(err) {
if (err) return res.apiError('create error', err);
res.apiResponse({
post: item
});
});
});
}
/**
* Delete Post by ID
*/
exports.remove = function(req, res) {
Post.model.findById(req.params.id).exec(function (err, item) {
if (err) return res.apiError('database error', err);
if (!item) return res.apiError('not found');
item.remove(function (err) {
if (err) return res.apiError('database error', err);
return res.apiResponse({
success: true
});
});
});
}
var _ = require('underscore'),
keystone = require('keystone'),
middleware = require('./middleware'),
importRoutes = keystone.importer(__dirname);
// Common Middleware
keystone.pre('routes', middleware.initLocals);
keystone.pre('render', middleware.flashMessages);
// Import Route Controllers
var routes = {
views: importRoutes('./views'),
api: importRoutes('./api')
};
// Setup Route Bindings
exports = module.exports = function(app) {
// Views
app.get('/', routes.views.index);
app.get('/blog/:category?', routes.views.blog);
app.get('/blog/post/:post', routes.views.post);
app.get('/gallery', routes.views.gallery);
app.all('/contact', routes.views.contact);
app.get('/api/post/list', keystone.initAPI, routes.api.posts.list);
app.all('/api/post/create', keystone.initAPI, routes.api.posts.create);
app.get('/api/post/:id', keystone.initAPI, routes.api.posts.get);
app.all('/api/post/:id/update', keystone.initAPI, routes.api.posts.update);
app.get('/api/post/:id/remove', keystone.initAPI, routes.api.posts.remove);
}
@ninjasort
Copy link

I am also curious as to why you're using res.apiResponse. I think it would be more elegant to use res.send and res.json no?

@askdesigners
Copy link

I think it's just a helper that makes it a lot easier to manage consistency in your responses. If you only have a couple of endpoints its value isn't obvious but when you have like 40 it's awesome to be able to just blindly pass the responder the err and data objects that come out of a query without manual formatting.

@kraigh
Copy link

kraigh commented Mar 29, 2015

I'm implementing this pattern for an API interface to my app, and am having trouble with a CloudinaryImage field. The API receives an image as a url, and I'm trying to get keystone to download the file, and then upload it to Cloudinary. Any ideas of what the easiest way to do this would be?

@atran
Copy link

atran commented Mar 31, 2015

How do I use keystone.middleware.cors with this? I've tried so many combinations...

@timondavis
Copy link

@atran Other than using the keystone.middleware.cors middleware in your API path declarations, be sure to include keystone.set( 'cors allow origin', true ) in your keystone.js config file. Worked for me, anyway :)

@timondavis
Copy link

I'm using the LocalFile field on a project that is on its way to becoming an API server. I was disappointed to see that LocalFiles will return the -absolute- path (relative to the site root) for file URLs out to the caller. Is there a way to get the proper URL for retrieving the image into the LocalFile JSON output?

@alsoicode
Copy link

Is there a way to automatically follow related objects? When I specify .populate('myField') in an API call:

Sample.model.find().populate('sampleImages').exec().then(...)

The sampleImages array will be empty. If I remove the .populate(), I get the IDs of the related objects. What am I doing wrong?

@alsoicode
Copy link

I was just missing:

Sample.add({
    . . .
    sampleImages: { type: Types.Relationship, ref: 'SampleImage', many: true }
});

Now it works as expected.

@alsoicode
Copy link

What should the Content-Type of POST requests be? application/x-www-form-urlencoded? If I set it to application/json an OPTIONS request is generated first and the req.body will be empty.

@victorcbr
Copy link

how can i do a sort with this example?

@jeffreypriebe
Copy link

@victorcbr Sorting would be in routes-api-posts.js.
Instead of a straight "find" in the list, add a mongoose sort. See StackOverflow, Mongoose 1, or Mongoose 2.
(Note: Mongoose links don't go straight to a sort example, not good link.)

Copy link

ghost commented Dec 14, 2015

am having trouble with a CloudinaryImage field. The API receives an image as a url, and I'm trying to get keystone to download the file, and then upload it to ...?

@alancwoo
Copy link

I seem to be having this error when trying to implement something along the lines of Line 28 of routes-api-posts.js

app.get('/api/post/:id', keystone.initAPI, routes.api.posts.get);

Error: Route.get() requires callback functions but got a [object Undefined]

Does anyone have any ideas? I'm using Node 4.1.2 and Keystone 0.3.16

EDIT
Ah, after a small amount of digging and reading:

keystonejs/keystone#836
https://github.com/keystonejs/keystone/wiki/0.2.x-to-0.3.x-Changes

and in combination with:

keystonejs/keystone#1135

app.get('/api/post/:id', keystone.middleware.api, routes.api.post.get);

Seemed to do the trick.

@liming
Copy link

liming commented Feb 18, 2016

Keystone.js 4.0 will change initAPI to:
app.all('/api*', keystone.middleware.api);

@jjhesk
Copy link

jjhesk commented Mar 30, 2016

im using your coding block now but its not working and it points out that initAPI is undefined. I am using the master git as my deployment now.

/**
 * This file is where you define your application routes and controllers.
 *
 * Start by including the middleware you want to run for every request;
 * you can attach middleware to the pre('routes') and pre('render') events.
 *
 * For simplicity, the default setup for route controllers is for each to be
 * in its own file, and we import all the files in the /routes/views directory.
 *
 * Each of these files is a route controller, and is responsible for all the
 * processing that needs to happen for the route (e.g. loading data, handling
 * form submissions, rendering the view template, etc).
 *
 * Bind each route pattern your application should respond to in the function
 * that is exported from this module, following the examples below.
 *
 * See the Express application routing documentation for more information:
 * http://expressjs.com/api.html#app.VERB
 */

const keystone = require('keystone');
const middleware = require('./middleware');
const importRoutes = keystone.importer(__dirname);

// Common Middleware
keystone.pre('routes', middleware.externalSchema);
keystone.pre('render', middleware.flashMessages);

keystone.set('404', function (req, res, next) {
    res.status(404).render('errors/404');
});

// Import Route Controllers
var routes = {
    api:importRoutes('./api'),
    views: importRoutes('./views'),
    download: importRoutes('./download')
};

// The imported routs for api
var api = {
    token: importRoutes('./api/').token,
    call: importRoutes('./api/call'),
    driver: importRoutes('./api/driver'),
    account: importRoutes('./api/me')
};

// Setup Route Bindings
exports = module.exports = function (app) {
    // Views
    app.get('/', routes.views.index);

    // app.get('/register/machine/', routes.views.blog);
    // app.get('/blog/post/:post', routes.views.post);
    // app.get('/ticket/:tid', routes.views.ticket);
    // app.get('/gallery', routes.views.gallery);
    //  app.all('/contact', routes.views.contact);
    //app.all('/api/*', keystone.middleware.api);
    //  app.get('/download/users', routes.download.users);

    app.get('/api/post/list', keystone.initAPI, routes.api.posts.list);
    app.all('/api/post/create', keystone.initAPI, routes.api.posts.create);
    app.get('/api/post/:id', keystone.initAPI, routes.api.posts.get);
    app.all('/api/post/:id/update', keystone.initAPI, routes.api.posts.update);
    app.get('/api/post/:id/remove', keystone.initAPI, routes.api.posts.remove);

    // jwt token authentication for socket.io traffic
    app.all('/api/token*', middleware.requireUser);
    app.all('/api/token', api.token);

    app.all('/api/call/new', api.call.new);
    app.all('/api/call/confirm', api.call.confirm_order);
    app.all('/api/call/check', api.call.check_order);
    app.all('/api/call/report', api.call.report);
    app.all('/api/call/status', api.call.status);

    app.all('/api/driver/list', api.driver.mylist);
    app.all('/api/driver/inquiry', api.driver.inquiry);
    app.all('/api/driver/login', api.account.login);
    app.all('/api/driver/new', api.account.newdriver);
    app.all('/api/driver/deal', api.driver.deal);
    app.all('/api/driver/prompt_customer', api.driver.listening_customer);
    app.all('/api/driver/release', api.driver.release_order);

    // app.all('/api/me/register', routes.api.register);
    // NOTE: To protect a route so that only admins can see it, use the requireUser middleware:
    // app.get('/protected', middleware.requireUser, routes.views.protected);
};

error


Warning: connect.session() MemoryStore is not
designed for a production environment, as it will leak
memory, and will not scale past a single process.
Error: Route.get() requires callback functions but got a [object Undefined]
    at Route.(anonymous function) [as get] (/Users/hesk/github/herokutaxione/node_modules/keystone/node_modules/express/lib/router/route.js:196:15)
    at EventEmitter.app.(anonymous function) [as get] (/Users/hesk/github/herokutaxione/node_modules/keystone/node_modules/express/lib/application.js:481:19)
    at module.exports (/Users/hesk/github/herokutaxione/routes/index.js:61:9)
    at createApp (/Users/hesk/github/herokutaxione/node_modules/keystone/server/createApp.js:118:25)
    at initExpressApp (/Users/hesk/github/herokutaxione/node_modules/keystone/lib/core/initExpressApp.js:5:46)
    at start (/Users/hesk/github/herokutaxione/node_modules/keystone/lib/core/start.js:47:7)
    at Object.<anonymous> (/Users/hesk/github/herokutaxione/keystone.js:88:10)
    at Module._compile (module.js:435:26)
    at Object.Module._extensions..js (module.js:442:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:311:12)
    at Function.Module.runMain (module.js:467:10)
    at startup (node.js:134:18)
    at node.js:961:3

@jstockwin
Copy link

Is it possible to set up this for the User model, and use it to allow non-admin users to update their details?
Obviously there would need to be some extra security to only allow the user to edit their own details.

I had a quick go, but ran into a validation error saying "Passwords should match" - presumably there needs to be some encryption using bcrypt first? If this won't work, is there any other way of being able to do this. What about if the user wants to change their password? Allowing non-admin users to update their account details seems like a common thing to want to do, but I can't find any examples of this anywhere.

@jstockwin
Copy link

@jjhesk, have you tried @liming's solution of changing keystone.initAPI to keystone.middleware.api ? It worked for me.

@VinayaSathyanarayana
Copy link

Will this be natively available in 0.4?

@sebastiancarlsson
Copy link

sebastiancarlsson commented Aug 2, 2016

I urgently need help with something, I created an example with a very simple list (of countries) and created api routes like instructed:

app.get('/api/countries', keystone.middleware.api, routes.api.countries.list);

import keystone from 'keystone';

export function list(req, res) {
  keystone.List('Country').model.find((err, items) => {
    if (err) return res.apiError('database error', err);

    res.apiResponse({
      countries: items
    });
  });
}

I get the error Cannot read property 'find' of undefined, the List object exists but it doesn't have a model property. Does anyone know why this is? The keystone admin UI works as expected and there are several objects in the database.

Update: I found the solution, the problem was that I used keystone.List (capitalized) instead of keystone.list. Hopefully someone else will be helped by this answer.

@Anuragpatel10
Copy link

Anyone here to explain ?
Example of sending file (csv) ?

@pipponippo
Copy link

pipponippo commented Sep 14, 2017

Hi there,

I'm having a hard time making my API work.
Models: I have a Destination model, with services attached to it, like this (in /models/Destination.js):

var Destination = new keystone.List('Destination', {
	map: { name: 'name' },
	track: true,
	nocreate: true,
	defaultSort: 'name',
});

Destination.add({
	/* Destination fields */
	name: { type: String, label: 'Nome', required: true, initial: true }, 
	code: { type: String, label: 'Codice', required: true, initial: true, index: true, unique: true, noedit: true }, 
	desc: { type: String, label: 'Descrizione'}
});

/**
 * Relationships
 */
Destination.relationship(
	{ path: 'hotels', ref: 'Service', refPath: 'destinationRef', filters: { serviceType: 'HO' }, many: true },
	{ path: 'bookings', ref: 'Booking', refPath: 'destinationRef' }
	);

I've built routes like this (in /routes/index.js):

	// public API
	app.all('/api*', keystone.middleware.api);
	app.get('/api/dest', keystone.middleware.api, routes.api.dest.getDestinations);
	app.get('/api/dest/:code', keystone.middleware.api, routes.api.dest.getDestinationByCode);

And finally I have my exported method in /api/dest.js, like this:

/**
 * Get Destination by code
 */
exports.getDestinationByCode = function(req, res) {
  var normCode = req.params.code.toUpperCase();
  // console.log("In API call: /api/dest/:code with "+normCode);

  Destination
    .model
    .where({'code': normCode})
/*    .populate({
      path: 'hotels',
      model: 'Service',
      select: 'code name overall',
      match: {
        'serviceType': 'HO'
      }
    })*/
    .populate('hotels')
    .select({'code': 1, 'name': 1, 'desc': 1})
    .findOne()
    .exec(function(err, item) {

      if (err) return res.apiError('database error', err);
      if (!item) return res.apiError('not found');

      res.apiResponse({
        collection: item
      });
    });
}

Now, what I receive back by calling /api/dest/:code is incomplete:
{"collection":{"_id":"59b424b5d7938827d40a755c","name":"Zanzibar","code":"ZNZ","desc":"Where Freddie Mercury was born!"}}
Where is my 'hotels' relationship? I do see relationships in AdminUI (almost as expected, 'filters' gets not respected though). But no matter what, the API call has no 'hotels' field. I've tried 'populate' in two versions (the alternative is the one commented above), I've tried using populateRelated, like this:

    ... .exec(function(err, item) {

      if (err) return res.apiError('database error', err);
      if (!item) return res.apiError('not found');

      item.populateRelated('hotels', function(err) {
        if (err) return res.apiError('populate error', err);
        console.log(item.hotels);
        // how do I use item.hotels??
      });

      res.apiResponse({
        collection: item
      });
...

This way, console.log shows me the correct list of result, but then I ran out of ideas about how to get back the result into returned item.

Can anyone help or suggest?
Running Keystone with DEBUG=keystone:* node keystone helped figure out a bit, but not enough.

Thanks in advance.
Filippo


**EDIT:
I've been able to hack this by manually populating the hotel list, like this:

exports.getDestinationByCode = function(req, res) {
  var normCode = req.params.code.toUpperCase();

  Destination
    .model
    .where({'code': normCode})
    .select({'code': 1, 'name': 1, 'desc': 1, '_id': 0})
    .findOne()
    .exec(function(err, item) {

      if (err) return res.apiError('database destination error', err);
      if (!item) return res.apiError('not found');

      Service
        .model
        .where({'destinationRef': item._id, 'serviceType': 'HO'})
        .select({'code': 1, 'name': 1, 'overall': 1, '_id': 0})
        .find(function(err, hotels) {

          if (err) return res.apiError('database service error', err);

          res.apiResponse({
            destination: item,
            hotels: hotels
          });
        });
    });
}

I would anyway expect that populate work, just to mantain data the way they are ('destination' should be a model with a 'hotels' array field).

Thanks for any suggestion!
F.

@milanffm
Copy link

milanffm commented Sep 30, 2017

Create thanks work fine :-)
I use it with Keystone 4.0

@linuxenko
Copy link

Why do you import async ?

@kobusan
Copy link

kobusan commented Jun 30, 2018

async is not require after nodeJS v7.6 :)

@kengres
Copy link

kengres commented Apr 5, 2019

what does keystone.middleware.api do?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment