Tutorials
Making a HackerNews clone in < 30 minutes using Neutrino
App Overview
What we'll be building:
Check out the demo We will build a forum where users can sign up, log in/out, create posts, like posts, comment on posts, and reply to comments. The app should also include a front page where you can view all posts and click on individual posts to view all comments. This is strictly a backend tutorial but if you want to follow along, feel free to clone the frontend code from GitHub.
Getting Started
Creating a Neutrino account
Register for a Neutrino account here (don't worry, its free!) After logging in, you should be prompted to this menu:
Click on 'start from an empty project'...
Setting Up the Models
Let's start the project by defining the models we're going to use. In the context of HackerNews NeutrinoNews, we're going to need Users, Posts, and Comments.
User
Users will have a username (string), email (string), and password (string).
Post
Posts should have a title (string), url (string), and content (string).
Comment
Users to have content (string) and a user (Object Id).
ℹ️ Make sure to leave 'initialize with with controller' checked when adding the Models.
Your Models page should look something like this:
Model Relationships
We want to define our database relationships to handle features such as likes and posting.
A user should be able to make posts, therefore User has many Posts and a Post belongs to User.
A user should be able to comment on posts. With this, there are a couple implementations, but for this tutorial, Post has many Comment and Comment belongs to Post. Remember we added a user parameter of type Object Id for Comment.
Implementing Likes
Likes will be implemented as a many-to-many relationship between User and Post. That is, a User has many Post (likedPosts), and Post has many User (likedBy).
Implementing Replies
We will add an additional, optional parameter to Comment called replyParent, which will be the Object Id of the parent Comment if the comment is a reply.
Your models page should now look something like this...
API Routes
Navigate to the Routes page. If you left the checkbox selected when creating the models through the modal, then Neutrino should have automatically implemented all the CRUD routes for User, Post, and Comment. That being said, there are still a couple of changes we need to make.
Your Routes page should look something like this...
Liking Posts
We want to create 2 functions for the User controller:
- likePost
- unlikePost
likePost HTTP method: POST url: /users/:id/like-post/:post_id
unlikePost HTTP method: POST url: /users/:id/unlike-post/:post_id
You can implement these functions by clicking on add route_ as follows:
...Do the same for unlikePost.
We can now write out the logic functions by clicking on the orange gear icon by the route.
Logic Liking posts works as follows:
We find the User with _id = id, and push post_id to its likedPosts array.
We then find the Post with _id = post_id and push id to its likedBy array.
We then return a success message or an error if anything didn't work.
We can do this pretty easily using the code blocks as follows:
...or if you'd rather code it yourself, this is what the code would look like using Mongoose:
likePost: async (req, res) => {
try {
const { id, post_id } = req.params;
User.findByIdAndUpdate(id,
{
$push: { likedPosts: post_id }
},
(err, data) => {
if (err) {
return res.status(500).send({ message: "Error updating user" });
};
Post.findByIdAndUpdate(post_id,
{
$push: { likedBy: id },
},
(err2, data2) => {
if (err2) {
return res.status(500).send({ message: "Error updating post" });
};
return res.status(200).send({ message: "Successfully liked post and added user to post likes" });
});
});
} catch(e) {
console.error(`server error in UserController likePost() : ${e}`);
};
},
unlikePost This function will be exactly the same except that instead of pushing, you will be pulling.
i.e.
$push: { likedBy: id },
will instead be:
$pull: { likedBy: id },
Creating Replies
We will write two functions for the Comment Controller:
- createReply
- viewReplies
createReply HTTP method: POST url: /comments/:id/create-reply Here, we want to create a new Comment, and pass in the additional replyParent = id parameter
viewReplies HTTP method: GET url: /comments/:id/replies Here, we want to find all comments where replyParent == id
If you are interested in coding them yourself, the code using Mongoose would look as follows...
/*
* createReply
* url: /comments/:id/create-reply
* params: ['id', 'reply_id']
*/
createReply: async (req, res) => {
try {
const { id } = req.params;
const { content, user, post } = req.body;
const newReply = await new Comment({
content,
user,
post,
replyParent: id,
}).save((err, data) => {
if (err) {
return res.status(500).send({ message: "Error creating comment" });
};
return res.status(200).send({ message: "Successfuly created reply comment" });
});
} catch(e) {
console.error(`server error in CommentController createReply() : ${e}`);
};
},
/*
* view replies
* url: /comments/:id/replies
*/
viewReplies: async (req, res) => {
const { id } = req.params
try {
const replies = await Comment.find({ replyTo: id });
return res.status(200).send(replies);
} catch(e) {
console.error(`server error in CommentController index() : ${e}`);
};
},
Authentication
Neutrino makes it very simple to include JWT authentication out of the box.
Navigate to Auth and click on Simple Authentication
This will generate an Auth Controller as well as logIn, register routes, and a verifyJWT middleware.
Security
Middlewares
Let's say we want to reserve a few features for only users that have signed in.
We can go back to the Routes page and add the verifyJWT middleware to the route to block a non-signed in user from calling the function.
ex. For creating, editing, and deleting posts, we can add the verifyJWT middleware as follows...
Exporting your project
By navigating to the Export tab, you can download your code as a Node, MongoDB, Express project following an MVC architecture, that should hopefully feel a bit intuitive to work on top off.