CodingNic

Server-Rendered Views

A Quick CRUD Example

Server-Rendered Views 15 min read

A Quick CRUD Example

Objectives

By the end of this lesson, you should be able to:

  • Build a small create-read-update-delete flow rendered entirely with Handlebars
  • Use res.redirect() after a form submission changes data
  • Explain why plain HTML forms only support GET and POST, and how that shapes route design

๐Ÿ’ก Why this matters: Lesson 5 covered Handlebars’ pieces individually, this lesson puts them together into the same shape a real admin tool takes: a list page, a create form, an edit form, and delete buttons, all server-rendered, no separate frontend framework involved.

โš ๏ธ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests, using express-handlebars 9.

Project File Structure

Here’s the whole app, before looking at any single file:

text
project/
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ server.js
โ””โ”€โ”€ views/
    โ”œโ”€โ”€ layouts/
    โ”‚   โ””โ”€โ”€ main.handlebars
    โ”œโ”€โ”€ index.handlebars      <- GET /tasks (the list)
    โ”œโ”€โ”€ new.handlebars        <- GET /tasks/new (create form)
    โ””โ”€โ”€ edit.handlebars       <- GET /tasks/:id/edit (edit form)

No partials folder this time, the app is small enough that every page stands alone, {{#each}}, {{#if}}, and the layout from Lesson 5 are enough on their own. A larger version of this app (shared navigation across pages, for example) would reach for a partials folder exactly as Lesson 5 covered.

The Views

html
<!-- views/layouts/main.handlebars -->
<!DOCTYPE html>
<html>
<head><title>Task List</title></head>
<body>
  <h1>Task List</h1>
  {{{body}}}
</body>
</html>
html
<!-- views/index.handlebars -->
<a href="/tasks/new">Add a task</a>
<ul>
  {{#each tasks}}
    <li>
      {{this.title}}
      <a href="/tasks/{{this.id}}/edit">Edit</a>
      <form action="/tasks/{{this.id}}/delete" method="POST" style="display:inline">
        <button type="submit">Delete</button>
      </form>
    </li>
  {{/each}}
</ul>
html
<!-- views/new.handlebars -->
<form action="/tasks" method="POST">
  <input type="text" name="title" placeholder="Task title">
  <button type="submit">Create</button>
</form>
html
<!-- views/edit.handlebars -->
<form action="/tasks/{{task.id}}/edit" method="POST">
  <input type="text" name="title" value="{{task.title}}">
  <button type="submit">Update</button>
</form>

Every piece here is straight from Lesson 5, the main layout wraps every page ({{{body}}}), {{#each tasks}} loops over the list, and each form is plain HTML, nothing Handlebars-specific about a <form> tag itself.

The Routes

javascript
const express = require('express');
const { engine } = require('express-handlebars');
const app = express();

app.engine('handlebars', engine());
app.set('view engine', 'handlebars');
app.use(express.urlencoded({ extended: true }));

let tasks = [
  { id: 1, title: 'Write lesson' },
  { id: 2, title: 'Verify code' }
];
let nextId = 3;

app.get('/tasks', (req, res) => {
  res.render('index', { tasks });
});

app.get('/tasks/new', (req, res) => {
  res.render('new');
});

app.post('/tasks', (req, res) => {
  tasks.push({ id: nextId++, title: req.body.title });
  res.redirect('/tasks');
});

app.get('/tasks/:id/edit', (req, res) => {
  const task = tasks.find(t => t.id === Number(req.params.id));
  if (!task) return res.status(404).send('Task not found');
  res.render('edit', { task });
});

app.post('/tasks/:id/edit', (req, res) => {
  const task = tasks.find(t => t.id === Number(req.params.id));
  if (!task) return res.status(404).send('Task not found');
  task.title = req.body.title;
  res.redirect('/tasks');
});

app.post('/tasks/:id/delete', (req, res) => {
  tasks = tasks.filter(t => t.id !== Number(req.params.id));
  res.redirect('/tasks');
});

app.listen(4304);

Six routes cover the whole flow: GET /tasks (list), GET /tasks/new (create form), POST /tasks (handle create), GET /tasks/:id/edit (edit form, pre-filled with the existing task), POST /tasks/:id/edit (handle update), POST /tasks/:id/delete (handle delete). This is the same route-parameter and body-parsing pattern from Module 5, just rendering views instead of returning JSON.

Trying It End to End

Creating a task, then confirming it appears:

bash
curl -X POST http://localhost:4304/tasks -d "title=Ship it"
curl http://localhost:4304/tasks
text
<li>Write lesson ...</li>
<li>Verify code ...</li>
<li>Ship it ...</li>

Updating it, then deleting a different one:

bash
curl -X POST http://localhost:4304/tasks/1/edit -d "title=Write lesson v2"
curl -X POST http://localhost:4304/tasks/2/delete
curl http://localhost:4304/tasks
text
<li>Write lesson v2 ...</li>
<li>Ship it ...</li>

Why POST for Update and Delete, and Why Redirect

Plain HTML forms only support GET and POST, there’s no native <form method="PUT"> or method="DELETE", this is why the edit and delete forms both POST, to a URL that describes the action (/tasks/:id/edit, /tasks/:id/delete) rather than relying on a different HTTP method (Module 9’s JSON APIs use real PUT/DELETE methods instead, since API clients aren’t limited to what an HTML form supports).

res.redirect(path), new in this lesson, sends a redirect response telling the browser to navigate to path. It’s called after every create, update, and delete, so the browser ends up back on the task list showing the fresh data, rather than sitting on a bare “form submitted” response, or, worse, resubmitting the same form again if the user hits refresh.

Try It

  1. Build this task list app from scratch, testing every route (list, new, create, edit, update, delete) with curl.
  2. Add a done boolean field to each task, and use {{#if this.done}} to show a checkmark next to completed tasks in the list.
  3. Add a confirmation step: instead of deleting immediately, render a small confirmation page first, with a form that POSTs to the actual delete route only once confirmed.
  4. Explain, in your own words, why res.redirect('/tasks') is called after each form submission instead of calling res.render('index', { tasks }) directly.

Recap

  • A server-rendered CRUD flow chains res.render() (to show a list or form) with res.redirect() (after a form submission changes data).
  • Plain HTML forms only support GET/POST, so update and delete routes are POST to an action-describing URL, not real PUT/DELETE.
  • The same route-parameter and body-parsing patterns from Module 5’s JSON API apply directly to a server-rendered app, only the response (a rendered view vs. JSON) differs.

Next lesson: choosing between server-rendered views and a JSON API.