Node API design and re-using code - javascript

My API has three endpoints: articles, websites, and users. Each article is associated with a website. A user can also share articles.
In my API, I have just created an endpoint at /website/:id/articles. This will query the database for articles associated with the given website. It then performs some manipulation on the data for each article based on who is talking to the API ("has the user shared this article?", for example).
I am now moving on to create a similar endpoint at /users/:id/shared-articles. The database query for this is slightly different, but the manipulation I want to perform on the articles data following the query is the same as before.
Here is some pseudo code for the former endpoint:
router.get('/websites/:id/articles', function (req, res) {
articleService.find({ websiteId: req.params.id }, function (error, foundArticles) {
async.waterfall([
function (cb) {
// Manipulate foundArticles…
cb(null, manipulatedArticles)
},
function (articles, cb) {
// Manipulate articles some more…
cb(null, manipulatedArticles)
},
], function (error, articles) {
if (error) {
return res.json(error, 400)
}
res.json(articles)
})
})
})
To create my new endpoint, /users/:id/shared-articles, I could abstract the manipulation tasks into a function that can be shared by both of my endpoints (the waterfall seen above), reducing code repetition.
router.get('/websites/:id/articles', function (req, res) {
articleService.find({ websiteId: req.params.id }, function (error, foundArticles) {
manipulateArticles(foundArticles, function (articles) {
if (error) {
return res.json(error, 400)
}
res.json(articles)
})
})
})
router.get('/users/:id/shared-articles', function (req, res) {
shareActionService.find({ userId: req.params.id }, function (error, foundShareActions) {
var sharedArticleIds = { _id: { $in: _.pluck(foundShareActions, 'sharedArticleId') } }
articleService.find(sharedArticleIds, function (error, foundArticles) {
manipulateArticles(foundArticles, function (articles) {
if (error) {
return res.json(error, 400)
}
res.json(articles)
})
})
})
})
However, I figured that this sort of code re-use problem must be common when designing APIs in Node, and I would like to know if there is an obviously better solution that I am missing here.
One idea I had would be to have all article sub-resources (such as /users/:id/shared-articles or /websites/:id/links) talk to the /links API internally, which itself would deal with the manipulation I mention above. The problem then is that I would have to make /links very verbose in the query headers/parameters it needs, in order to allow for the different database queries needed (such as those by the two sub-resource endpoints demonstrated here).
Is there a better solution/abstraction here?

You can create a "service" layer. Abstract the link manipulation into a completely separate file and call it from each of the routes.
Create a service/links.js:
module.exports = {
manipulateLinks: function (response) {
// Manipulate code
return response
}
}
Then in you routes, call the function:
var linkservice = require('../service/links')
var response = linkservice.manipulateLinks(response)

Related

Can we group queries/resolvers in Graphql and then add Middleware?

I want to ask about graphql.
In graphql, can we group query/resolver and then give middleware like laravel? If it's Laravel, we can group routes and then the group can be given middleware like this.
Route::middleware(['auth'])->group(function () {
Route::get('/', function () { ... });
Route::get('/blog', function () { ... });
});
Is providing one-by-one authentication in query/resolver the best way? Or is there a better way than providing authentication one by one? I want it so that there is no code duplication so the code is clean. I was thinking about making a function and then adding it to each query/resolver, but I'm not sure if this is the best way or not.
const resolvers = {
Query: {
blog: (_, { id, slug }) => {
if (!user) {
throw new Error("Unauthenticated!")
}
// return ...
},
blogs: (_, args, { user }) => {
if (!user) {
throw new Error("Unauthenticated!")
}
// return ...
}
}
}
Sorry if there are wrong words, because I am also still new to Graphql and not very good at English.

how to break logic into a controller and a model in a node

I do not quite understand how to properly break the logic on the controllers and models in nodeJS when working with the backend application. Suppose I have an example
This code is in the model of my application, and logically I understand that the model is only responsible for choosing from the database, and the controller and everything else should be done by the controller, but I don’t quite understand how to do this and I tried to transfer part of the code to the controller and export it, but I did not succeed (Please, help, at least with this example! The main thing for me is to understand the principle of working with MVC in the node !!!
exports.currentPostPage = function(req, res){
db.query('SELECT * FROM `posts`', function (err, result) {
if (err){
console.log(err);
}
var post = result.filter(item => {return (item.id == req.params.id)? item: false})[0];
if (post === undefined){
res.render('pages/404');
} else {
res.render('pages/post-page', {postId: req.params.id, item: post});
}
});
};
So, you're on the right track. There's a lot of different ways to do it depending on preferences, but one pattern I've seen pretty commonly is to use the callback as a way to integrate. For example, let's say you have your model file:
exports.getPostById = (id, cb) => {
db.query('SELECT * FROM `posts` WHERE id=?', [id], function (err, result) {
if (err){
return cb(err); // or, alternatively, wrap this error in a custom error
}
// here, your logic is just returning whatever was returned
return cb(null, result);
});
};
Note I also am letting the DB handling the ID lookup, as it's probably more efficient at doing so for larger data sets. You didn't say what DB module you're using, but all the good ones have some way of doing parametrized queries, so use whatever works w/ your DB driver.
Anyway, the Model file therefore handles just the data interaction, the controller then handles the web interaction:
// postController.js
const model = require('../models/postModel.js'); // or whatever you named it
exports.populatePost = (req, res, next, id) => {
model.getPostById(id, (err, post) => {
if (err) return next(err); // centralized error handler
req.post = post;
next();
});
}
export.getOnePost = (req, res, next) => {
if (req.post) {
return res.render('pages/post-page', req.post);
}
// again, central error handling
return next({ status: 404, message: 'Post not found' });
}
I have mentioned central error handling; I vastly prefer it to scattering error handling logic all over the place. So I either make custom errors to represent stuff, or just do like above where I attach the status and message to an anonymous object. Either will work for our purposes. Then, in a middleware file you can have one or more handler, the simplest like this:
// middleware/errors.js
module.exports = (err, req, res, next) => {
console.error(err); // log it
if (err.status) {
return res.status(err.status).render(`errors/${err.status}`, err.message);
}
return res.status(500).render('errors/500', err.message);
}
Finally, in your routing setup you can do things like this:
const postController = require('../controllers/postController');
const errorHandler = require('../middleware/errors.js');
const postRouter = express.Router();
postRouter.param('postId', postController.populatePost);
postRouter.get('/:postId', postController.getOnePost);
// other methods and routes
app.use('/posts', postRouter)
// later
app.use(errorHandler);
As was pointed out in the comments, some folks prefer using the Promise syntax to callbacks. I don't personally find them that much cleaner, unless you also use the async/await syntax. As an example, if your db library supports promises, you can change the model code to look like so:
exports.getPostById = async (id, cb) => {
// again, this assumes db.query returns a Promise
return await db.query('SELECT * FROM `posts` WHERE id=?', [id]);
}
Then your controller code would likewise need to change to handle that as well:
// postController.js
const model = require('../models/postModel.js'); // or whatever you named it
exports.populatePost = async (req, res, next, id) => {
try {
const post = await model.getPostById(id)
req.post = post
return next()
} catch (err) {
return next(err)
}
}

Consume JSON from a URL in Express

The question is: How can I import json from a URL specifically, NOT an internal file in Express, and contain it such that I can use it across multiple views. For example, I have a controller. How can I get in in there (controller)? I am using request.
I have a router with 2 routes but I want to have a bunch more, and the bulk of the logic for the routes is being done in controllers.
Below is a controller with the route for showing all. I had hardcoded a small piece of "json" in it as data to use temporarily, but now I want to populate my view via an outside api. This is my controller:
module.exports = {
//show all USERS
showDogs: (req,res) => {
const dogs = [
{
name:"Fluffy", breed:"ChowChow", slug:"fluffy", description:"4 year old Chow. Really, really fluffy."
},
{
name:"Buddy", breed:"White Lab", slug:"buddy", description:"A friendly 6 year old white lab mix. Loves playing ball"
},
{
name: "Derbis", breed:"Schmerbis",slug:"derbis", description:"A real Schmerbis Derbis"
}
];
res.render("pages/dogs", {dogs: dogs, title:"All Dogs"});
}
};
How can I get this json the data to come from an outside line? I have used request before but I don't know how to transfer the data between files. I don't want to put it inside the showDogs or it won't be accessible to other functions here. Right?
I had something like this below, with require('request') at the top of the controller, but it just gave errors.
const options = {
url:'https://raw.githubusercontent.com/matteocrippa/dogbreedjsondatabase/master/dog-breed.json',
method:'GET',
headers:{
'Accept-Charset': "utf-8"
NO IDEA ABOUT THIS AREA FOR NOW EITHER
}
I also tried wrapping the entire thing, all the functions, in a request:
request('https://raw.githubusercontent.com/matteocrippa/dogbreedjsondatabase/master/dog-breed.json', function(error, response, body)
But still I got an error.
And this the route.js where the controller sends:
//dogs
router.get('/dogs', dogsController.showDogs)
I am a Node beginner so the only thought I have is to write some middleware. The deeper problem here is I don't know how to use/write middleware properly. Perhaps I can become informed.
Add a utility file that contains the code to talk to the external API. Include this file and use it's function to get dogs data. Later, you can add more functions for other APIs as well.
const getDogData = require('../externalApis').getDogData;
module.exports = {
//show all USERS
showDogs: (req, res) => {
getDogData(function(err, dogs) {
if (err) {
//handle err
} else {
res.render("pages/dogs", {
dogs: dogs,
title: "All Dogs"
});
}
}
}
};
// externalApis.js
const request = require ('request');
module.exports = {
getDogData: function(done) {
const options = {
url: 'https://raw.githubusercontent.com/matteocrippa/dogbreedjsondatabase/master/dog-breed.json',
method: 'GET',
headers: {
'Accept-Charset': "utf-8"
}
}
request(options, function(error, response, body) {
if (error) {
return done(err);
} else {
var data = JSON.parse(body); // not sure how's data is returned or if it needs JSON.parse
return done(null, data.dogs); //return dogs
}
});
}

How to query all articles from a specific user?

CODE:
server-side
/**
* List of Articles
*/
exports.list = function (req, res) {
Article.find({ 'user.displayName': 'GIGANTOR !' }).sort('-created').populate('user', 'displayName').exec(function (err, articles) {
if (err) {
return res.status(422).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(articles);
}
});
};
SITUATION:
What I tried above does not work. I checked the mongoose docs: http://mongoosejs.com/docs/queries.html
but can't seem to get the query to work. Currently, the query just returns nothing.
QUESTION:
How to query all articles by a user with a specific displayName ?
TL;DR You can't query a document by a field that belongs to a populated object.
Since article simply has a ref to User, you'll have just get all articles, and then filter them in memory. Or, since the article.user field is an _id, you can find articles by the user ID (but your question is asking about finding them by user.displayName).
Mongoose populate does not do the populating in the MongoDB server itself; it populates on the application server. This means that multiple round-trips to the database are happening (see article Understanding Mongoose Population.) Therefore, you can't query by a field that exists as part of a populated object.
So, here's your 2 solutions:
Article.find({}).sort('-created').populate('user', 'displayName').exec(function (err, articles) {
if (err) {
return res.status(422).send({
message: errorHandler.getErrorMessage(err)
});
} else {
let filteredArticles = articles
.filter(article => article.user.displayName === 'GIGANTOR !');
res.json(filteredArticles);
}
});
Or, if you can query by _id, you can do this:
Article.find({ user: 'somemongoobjectidofuser' }).sort('-created').populate('user', 'displayName').exec(function (err, articles) {
if (err) {
return res.status(422).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(articles);
}
});
It gets to be a bit hairy and out of scope of the question, but another solution is the aggregation pipeline, which is only usually recommended for backend analytics. But, it'll provide you more flexibility in your query (especially if you user MongoDB's new $graphLookup).
Or, you can always store a copy of the user as a denormalized object inside the article document itself, but then you run into the much-discussed issue of maintaining denormalized documents in-sync.
Just putting the code I ended up using here for people who could need it:
/**
* List of Articles
*/
exports.list = function (req, res) {
Article.find({ user: req.user._id.toString() }).sort('-created').populate('user', 'displayName').exec(function (err, articles) {
if (err) {
return res.status(422).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.json(articles);
}
});
};

Access API endpoints in MEANjs from server controller

so i have this problem i am working on 'following' feature in my application. What's important, i have two models:
Follows and Notifications
When I hit follow button in front-end I run function from follow.client.controller.js which POSTs to API endpoint /api/follows which corresponds to follow.server.controller.js and then update action on Follows model is performed - easy. AFAIK thats how it works (and it works for me).
But in follows.server.controller.js I want also invoke post to API endpoint at /api/notifications which corresponds to notifications.server.controller.js but I can't find a proper way to do that. Any help will be appreciated.
I don't want another call from front-end to add notification because it should be automatic = if user starts following someone, information is saved in both models at once.
You can add middleware in your server route.
app.route('/api/follows')
.post(notification.firstFunction, follows.secondFunction);
And now add 2 methods in your contollers. First makes the call to db and add's some result's data to request object which will be forwarded to second method.
exports.firstFunction= function(req, res, next) {
Notification.doSometing({
}).exec(function(err, result) {
if (err) return next(err);
req.yourValueToPassForward = result
next(); // <-- important
});
};
exports.secondFunction= function(req, res) {
//...
};
Or you can make few database calls in one api method, joining this calls with promises. Example:
var promise = Meetups.find({ tags: 'javascript' }).select('_id').exec();
promise.then(function (meetups) {
var ids = meetups.map(function (m) {
return m._id;
});
return People.find({ meetups: { $in: ids }).exec();
}).then(function (people) {
if (people.length < 10000) {
throw new Error('Too few people!!!');
} else {
throw new Error('Still need more people!!!');
}
}).then(null, function (err) {
assert.ok(err instanceof Error);
});

Categories

Resources