DEV Community

Cover image for Lets build a HackerNews clone in <= 30 minutes
Ricardo Gonzalez
Ricardo Gonzalez

Posted on • Updated on

Lets build a HackerNews clone in <= 30 minutes

A MERN Stack Tutorial Using Neutrino

Preface

I love coding, but building CRUD apps and authentication systems can get pretty repetitive over time.

Tools like Ruby on Rails and Django have made this process significantly easier and more enjoyable, however, there is still a lot of repetitiveness I would ideally like to avoid.

Neutrino makes it significantly faster to code backend architectures. It gives you a nice, intuitive interface to design your backend system and automatically implement most of the code for you using Node/Express best practices and an MVC architecture.

If you don't know what HackerNews is, its a Reddit-esque forum built by YCombinator to talk about entrepreneurship.

This tutorial will be demonstrating how to use Neutrino to build a simplified HackerNews clone in under 30 minutes.

 

What is the MERN Stack

MERN stack refers to MongoDB, Express, React, and Node:
Mongo - A popular no-sql database program
Express - A backend JavaScript web application framework
React - A front-end JavaScript library for building user interfaces
Node - An open source JavaScript runtime environment

 

MVC Architecture

MVC is an architectural pattern for building software and web applications that consists of 3 parts, the Model, the View, and the Controller

Model - handles all data logic and directly interacts with the database. In this case, we will be using MongoDB and Mongoose, which is a library built on top of Mongo that we will use to define our model schema and interact with our Express server
View - handles all client side logic, this is the React side of the application and will be what the user interacts with
Controller - acts as an interface between the Model and View. It processes all requests, fetches data from the Model to send to the View, and takes in information from the View to update the Model

 

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:

Neutrino Homepage

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 parameters

 

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...

Models page with relations

 

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...

Routes page initial

 

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:

Image description

...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:

logic blocks for likePost

...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}`);
    };
  },
Enter fullscreen mode Exit fullscreen mode

 
unlikePost
This function will be exactly the same except that instead of pushing, you will be pulling.

i.e.

$push:  { likedBy: id },
Enter fullscreen mode Exit fullscreen mode

will instead be:

$pull:  { likedBy: id },
Enter fullscreen mode Exit fullscreen mode

 

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

createReply code blocks

viewReplies
HTTP method: GET
url: /comments/:id/replies
Here, we want to find all comments where replyParent == id

viewReplies code blocks

 
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}`);
    };
  },
Enter fullscreen mode Exit fullscreen mode

 

Authentication

Neutrino makes it very simple to include JWT authentication out of the box.

Navigate to Auth and click on Simple Authentication

Image description

 
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...

Image description

 

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.

 

Finishing Thoughts

Okay there's a chance after reading everything and probably making some changes, this project took a little more than 30 minutes.
But really, we had the bulk of our backend functionality up and running in only a couple of minutes using Neutrino.

There's obviously a lot more that can be done fixing up the app and adding more features, but hopefully after following these examples, you gathered enough on your own to get started with the rest.

Happy Coding!

Top comments (0)