Lesson 6: Blog CRUD

1 Use the filesystem

We want to store our blog posts properly, in a separate file. The JSON format allows us to have an object in a file of its own.

Let’s create an empty blog-posts.json file with an empty object for now: {}

Let’s remove the blogPosts object we created earlier, and let’s import the fs dependency (which stands for file system).

var fs = require('fs');

// Load our blog-posts.json file
var blogPosts = require('./blog-posts');
Copy to clipboardapp.js

2 Create the "Create Post" template

Like we did for the Contact page, we can use an HTML form to handle user input.

Let’s add some handlebars blocks to handle all the error cases.

<!DOCTYPE html>
<html>
  <head>
    <title>Create a new post</title>
    {{> head}}
  </head>
  <body>
    {{> header}}

    <main class="main blog">
      <div class="container">
        <h1 class="heading">
          <a href="/blog">Blog</a>
          <strong>Create post</strong>
        </h1>

        {{#if error}}
          <p class="error">{{message}}</p>
        {{/if}}

        {{#if success}}
          <p class="success">
            {{message}}
            <a href="/blog/{{postId}}">View post</a>
          </p>
        {{/if}}

        <form class="form" method="post" action="/create-post">
          <div class="field">
            <label class="label">Title</label>
            <input class="input" type="text" name="title" value="{{formBody.title}}">
          </div>
          {{#if missingTitle}}
            <p class="missing">The title is required.</p>
          {{/if}}

          <div class="field">
            <label class="label">Excerpt</label>
            <input class="input" type="text" name="excerpt" value="{{formBody.excerpt}}">
          </div>

          <div class="field">
            <label class="label">Content</label>
            <textarea class="textarea" name="content">{{formBody.content}}</textarea>
          </div>

          <div class="field no-label">
            <button class="button green">Create post</button>
          </div>
        </form>
      </div>
    </main>

    {{> footer}}
  </body>
</html>
Copy to clipboardcreate-post.handlebars

3 Add JSON helpers

As we are saving our blog posts by writing a file on the disk, we need to create two functions that will simplify that process (as we are going to use it for both creating and editing blog posts).

The first function will take a JavaScript object, and convert that into a valid JSON object. Although they look similar, there are slight differences. Luckily, we can use the JSON.stringify function.

The second function takes a Post object, and adds it to the blogPosts object, and then saves it to the blog-posts.json file.

function saveJSONToFile(filename, json) {
  // Convert the blogPosts object into a string and then save it to file
  fs.writeFile(filename, JSON.stringify(json, null, '\t') + '\n', function(err) {
    // If there is an error let us know
    // otherwise give us a success message
    if (err) {
      throw err;
    } else {
      console.log('It\'s saved!');
    }
  });
}

function savePost(id, object, data) {
  // Update object with new data
  object[id] = data;
  // Save the updated object to file
  saveJSONToFile('blog-posts.json', object);
}
Copy to clipboardapp.js

4 Add the /create-post routes

We need to handle both GET and POST requests to the /create-post route.

Let’s first retrieve the form body, and handle the case if the title is empty. If it is, we render the same template but with an error message, and the form body that was already inserted by the user.

// Create post page
app.get('/create-post', function(req, res) {
  res.render('create-post');
});

// Handle /create-post form submission
app.post('/create-post', function(req, res) {
  var formBody = {
    'title': req.body.title,
    'excerpt': req.body.excerpt,
    'content': req.body.content,
  };

  if (!formBody.title) {
    return res.render('create-post', {
      error: true,
      message: 'The title is required!',
      missingTitle: true,
      formBody,
    });
  }
});
Copy to clipboardapp.js

5 Save the post

If the title is present, we can save the post in the blog-posts.json file. Because the JSON is an object with unique keys, and as the Post IDs must be unique as well (so that we don’t override previously createds posts), we are going to use the current timestamp as our unique identifier.

// Use the timestamp as a unique identifier
var timestamp = Date.now();
var postId = timestamp;

// Save new post to file
savePost(postId, blogPosts, {
  id: postId,
  title: formBody.title,
  excerpt: formBody.excerpt,
  content: formBody.content,
});

res.render('create-post', {
  success: true,
  message: 'New post successfully created!',
  postId,
});
Copy to clipboardapp.js

6 Create the "Edit Post" template

Because it’s easy to make mistakes when creating a post, we also want to be able to edit them.

Let’s create an edit-post.handlebars template, which is almost identical to the create-post.handlebars one.

<!DOCTYPE html>
<html>
  <head>
    <title>Edit a post</title>
    {{> head}}
  </head>
  <body>
    {{> header}}

    <main class="main blog">
      <div class="container">
        <h1 class="heading">
          <a class="button blue" href="/blog/{{postId}}">View post</a>
          <a href="/blog">Blog</a>
          <strong>Edit post</strong>
        </h1>

        {{#if error}}
          <p class="error">{{message}}</p>
        {{/if}}

        {{#if success}}
          <p class="success">{{message}}</p>
        {{/if}}

        <form class="form" method="post" action="/edit-post/{{postId}}">
          <div class="field">
            <label class="label">Title</label>
            <input class="input" type="text" name="title" value="{{formBody.title}}">
          </div>
          {{#if missingTitle}}
            <p class="missing">The title is required.</p>
          {{/if}}

          <div class="field">
            <label class="label">Excerpt</label>
            <input class="input" type="text" name="excerpt" value="{{formBody.excerpt}}">
          </div>

          <div class="field">
            <label class="label">Content</label>
            <textarea class="textarea" name="content">{{formBody.content}}</textarea>
          </div>

          <div class="field no-label">
            <button class="button green">Save changes</button>
          </div>
        </form>
      </div>
    </main>

    {{> footer}}
  </body>
</html>
Copy to clipboardedit-post.handlebars

7 Add the /edit-post GET route

When we edit a post, it already exists. The same way we use the :post_id in the URL to show a post, we need it to edit it, so we can retrieve it from the blog-posts.json.

We can then pass the post object to the formBody, so that it’s displayed in the template.

// Edit a blog post
app.get('/edit-post/:post_id', function(req, res) {
  var postId = req.params['post_id'];
  var post = blogPosts[postId];

  if (!post) {
    res.send('Not found');
  } else {
    res.render('edit-post', {
      formBody: post,
      postId,
    });
  }
});
Copy to clipboardapp.js

8 Add the /edit-post POST route

When we submit the HTML form, we need to check again if the title is empty. The logic is similar to the /create-post route.

If the title is present, we can then use the form body to save the new edited post.

// Handle editing a blog post
app.post('/edit-post/:post_id', function(req, res) {
  var postId = req.params['post_id'];
  var post = blogPosts[postId];

  var formBody = {
    'title': req.body.title,
    'excerpt': req.body.excerpt,
    'content': req.body.content,
  };

  if (!formBody.title) {
    return res.render('edit-post', {
      error: true,
      message: 'The title is required!',
      missingTitle: true,
      postId,
      formBody,
    });
  }

  var newPost = {
    id: postId,
    title: formBody.title,
    excerpt: formBody.excerpt,
    content: formBody.content,
  };

  // Save new post to file
  savePost(postId, blogPosts, newPost);

  res.render('edit-post', {
    success: true,
    message: 'Post successfully saved!',
    postId,
    formBody: newPost,
  });
});
Copy to clipboardapp.js

9 Create the "Delete Post" template

There are many ways to delete data. We are going to implement a two-step process, in order to avoid user retribution.

First, we need a template to ask for confirmation to delete a post. The template will only show the post’s title and two buttons.

<!DOCTYPE html>
<html>
  <head>
    <title>Delete a post</title>
    {{> head}}
  </head>
  <body>
    {{> header}}

    <main class="main blog">
      <div class="container">
        <h1 class="heading">
          <a class="button blue" href="/blog/{{postId}}">View post</a>
          <a href="/blog">Blog</a>
          <strong>Delete post</strong>
        </h1>

        <form class="form" method="post" action="/delete-post/{{postId}}">
          <p class="confirm">
            Are you sure you want to delete the post <strong>{{postTitle}}</strong>?
          </p>

          <p>
            <button class="button red">Delete post</button>
            <a class="button" href="/blog/{{postId}}">Cancel</a>
          </p>
        </form>
      </div>
    </main>

    {{> footer}}
  </body>
</html>
Copy to clipboarddelete-post.handlebars

10 Add the /delete-post GET route

The same way we retrieve a post to edit it, we retrieve it before deleting it thanks to the :post_id parameter.

In the template, we only need the postId and the postTitle.

// We use this route when we want to delete a post
app.get('/delete-post/:post_id', function(req, res) {
  var postId = req.params['post_id'];
  var post = blogPosts[postId];

  if (!post) {
    res.send('Not found');
  } else {
    res.render('delete-post', {
      postId,
      postTitle: post.title,
    });
  }
});
Copy to clipboardapp.js

11 Add the /delete-post POST route

If the user clicks on “Delete post”, it submits a POST request to /delete-post with the post_id.

With this id, we know which post to delete from the blogPosts object.

We can then use this altered blogPosts object and save it again.

As this /delete-post/:post_id doesn’t exist anymore, let’s redirect to /blog with a notification.

// We use this route when we want to delete a post
app.post('/delete-post/:post_id', function(req, res) {
  var postId = req.params['post_id'];

  // Delete the post from the blogPosts object
  delete blogPosts[postId];

  // Save the updated object to file
  saveJSONToFile('blog-posts.json', blogPosts);

  // Tell the browser we're done
  res.redirect('/blog?delete=true');
});
Copy to clipboardapp.js

12 Handle the delete query in the /blog route

So far, we’ve used URL parameters with req.params.

Another way to pass data to a route is by using URL queries: req.query.

We are checking if the ?delete parameter is in the URL. As it’s a boolean, we can pass it directly to the template.

app.get('/blog', function(req, res) {
  res.render('blog', {
    posts: listOfPosts,
    delete: req.query['delete'],
  });
});
Copy to clipboardapp.js

13 Show the notification in the Blog template

In the blog.handlebars template, we can simply show a block if delete is true.

{{#if delete}}
  <p class="success">Post successfully deleted.</p>
{{/if}}
Copy to clipboardblog.handlebars