wordpress_blog

This is a dynamic to static website.

Express Crash Course (2024 Revamp)

Learning From Youtube Channel: Traversy Media
Video: Express Crash Course (2024 Revamp)
Thank you.

Timestamps:
00:00:00 – Intro & Slides
00:00:53 – What is Express?
00:02:28 – Opinionated vs Unopinionated
00:04:10 – Prerequisities
00:05:18 – What we’ll cover
00:06:45 – Express Setup
00:09:00 – Basic Server
00:13:10 – –watch Flag & NPM Scripts
00:15:25 – res.sendFile() Method
00:18:00 – Static Web Server
00:19:48 – Working with JSON
00:22:35 – Postman Utility
00:23:45 – Environment Variables (.env)
00:26:30 – Request Params (req.params)
00:29:35 – Query Strings (req.query)
00:33:19 – Setting Status Codes
00:36:40 – Multiple responses
00:37:35 – Route Files
00:41:40 – Using ES Modules
00:43:47 – Request Body Data
00:47:53 – POST Request
00:50:23 – PUT Request
00:53:23 – DELETE Request
00:55:44 – Middleware
01:00:24 – Custom Error Handler
01:08:30 – Catch All Error Middleware
01:10:47 – Colors Package
01:14:17 – Using Controllers
01:20:45 – __dirname Workaround
01:24:29 – Making Requests From Frontend
01:30:03 – Submit Form to API
01:36:00 – EJS Template Engine Setup
01:41:15 – Pass Data to Views
01:42:50 – Pass and Loop Over Arrays
01:44:20 – Template Partials

Intro & Slides

What Is Express?

  • Minimal and flexible web framework for Node.js
  • Used for building server-side web applications and APIs
  • The most popular framework for Node
  • Simplifies the process of handling HTTP requests and responses
  • Express is a very fast and unopinionated framework

Opinionated vs Unopinionated

Opinionated

  • Suggested ways to do things
  • Usually offer a lot of bells & whistles
  • Strict folder structure

Laravel、django、Rails

Unopinionated

  • Different ways to do the same thing
  • Includes the bare necessities
  • Structure folders how you want

Express、Flask、Sinatra

Prerequisites

  • JavaScript fundamentals (Functions, loops, objects, classes, etc)
  • HTTP Basics (Methods, status codes, etc)
  • How JSON APIs work
  • Basics understanding of Node.js
  • NPM (Node Package Manager)

What we’ll cover

Covered In This Course

  • Routing
  • Request & Response
  • Custom Middleware
  • CRUD Operations
  • Template Engines
  • Error Handling
  • 3rd Party NPM Packages
  • Controllers
  • Fetching From Frontend
  • Environment Variables

Express Setup

  • Express.js
  • 建立 package.json 檔案 – npm init -y
  • 安裝 Express 套件- npm i express
  • 建立 .gitignore 檔案
// .gitignore
node_modules/
.env

Basic Server

  • 建立進入點 server.js 檔案
  • 終端機執行 node server.js
// server.js
const express = require('express');

const app = express();

app.get('/', (req, res) => {
  res.send({ message: 'Hello World' });
});

app.listen(8000, () => console.log(`Server is running on port 8000`));

–watch Flag & NPM Scripts

  • 也可以選擇安裝 nodemon 套件
  • 現在 Node.js 可以使用 –watch Flag
  • 修改 package.json 檔案
  • 修改 server.js 檔案
  • 終端機執行 – npm run dev
// package.json
{
  "name": "express_crash",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node server",
    "dev": "node --watch server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.19.2"
  }
}
// server.js
const express = require('express');

const app = express();

app.get('/', (req, res) => {
  res.send('Hello World');
});

app.get('/about', (req, res) => {
  res.send('About');
});

app.listen(8000, () => console.log(`Server is running on port 8000`));

res.sendFile() Method

  • 建立 public 資料夾來放置關於 HTML 相關檔案
  • 在 public 資料夾內建立 index.html、about.html 檔案
  • 修改 server.js 檔案
// public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Welcome</title>
</head>
<body>
  <h1>Welcome to the homepage</h1>
</body>
</html>
// public/about.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>About</title>
</head>
<body>
  <h1>Welcome to the about page</h1>
</body>
</html>
// server.js
const express = require('express');
const path = require('path');

const app = express();

app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

app.get('/about', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'about.html'));
});

app.listen(8000, () => console.log(`Server is running on port 8000`));

Static Web Server

  • 修改 server.js 檔案
    setup static folder 只有 index.html 檔案能正常使用
// server.js
const express = require('express');
const path = require('path');

const app = express();

// setup static folder
app.use(express.static(path.join(__dirname, 'public')));

app.listen(8000, () => console.log(`Server is running on port 8000`));

Working with JSON

  • 路徑: http://localhost:8000/api/posts
// server.js
const express = require('express');
const path = require('path');

const app = express();

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

app.get('/api/posts', (req, res) => {
  res.json(posts);
});

app.listen(8000, () => console.log(`Server is running on port 8000`));

Postman Utility

  • 使用 Postman 套件
  • 我選擇使用 Hoppscotch

Environment Variables (.env)

  • 建立 .env 檔案,PORT=8080、PORT=8000 前後的變化
  • 修改 server.js 檔案
  • 修改 package.json 檔案
// .env
PORT = 8000
// server.js
const express = require('express');
const path = require('path');
const port = process.env.PORT || 8000;

const app = express();

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

app.get('/api/posts', (req, res) => {
  res.json(posts);
});

app.listen(port, () => console.log(`Server is running on port ${port}`));
// package.json
{
  "name": "express_crash",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node server",
    "dev": "node --watch --env-file=.env server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.19.2"
  }
}

Request Params (req.params)

  • 修改 server.js 檔案
  • 使用 API 測試工具
// server.js
const express = require('express');
const path = require('path');
const port = process.env.PORT || 8000;

const app = express();

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
app.get('/api/posts', (req, res) => {
  res.json(posts);
});

// Get single post
app.get('/api/posts/:id', (req, res) => {
  const id = parseInt(req.params.id);
  res.json(posts.filter((post) => post.id === id));
});

app.listen(port, () => console.log(`Server is running on port ${port}`));

Query Strings (req.query)

  • 查詢字串
    http://localhost:8000/api/posts?limit=2
    http://localhost:8000/api/posts?limit=2&sort=desc
  • 修改 server.js 檔案
  • 使用 API 測試工具
// server.js
const express = require('express');
const path = require('path');
const port = process.env.PORT || 8000;

const app = express();

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
app.get('/api/posts', (req, res) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    res.json(posts.slice(0, limit));
  } else {
    res.json(posts);
  }
});

// Get single post
app.get('/api/posts/:id', (req, res) => {
  const id = parseInt(req.params.id);
  res.json(posts.filter((post) => post.id === id));
});

app.listen(port, () => console.log(`Server is running on port ${port}`));

Setting Status Codes

  • 修改 server.js 檔案
  • 使用 API 測試工具
    http://localhost:8000/api/posts/1
    http://localhost:8000/api/posts/2
    http://localhost:8000/api/posts/200
// server.js
const express = require('express');
const path = require('path');
const port = process.env.PORT || 8000;

const app = express();

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
app.get('/api/posts', (req, res) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    res.status(200).json(posts.slice(0, limit));
  } else {
    res.status(200).json(posts);
  }
});

// Get single post
app.get('/api/posts/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    res.status(404).json({ msg: `A post with the id of ${id} was not found` });
  } else {
    res.status(200).json(post);
  }
});

app.listen(port, () => console.log(`Server is running on port ${port}`));

Multiple responses

// server.js
const express = require('express');
const path = require('path');
const port = process.env.PORT || 8000;

const app = express();

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
app.get('/api/posts', (req, res) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    return res.status(200).json(posts.slice(0, limit));
  }
  res.status(200).json(posts);
});

// Get single post
app.get('/api/posts/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }
  res.status(200).json(post);
});

app.listen(port, () => console.log(`Server is running on port ${port}`));

Route Files

  • 建立 routes 資料夾
  • 在 routes 資料夾裡面建立 posts.js 檔案
  • 修改 server.js 檔案
// routes/posts.js
const express = require('express');
const router = express.Router();

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
router.get('/', (req, res) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    return res.status(200).json(posts.slice(0, limit));
  }
  res.status(200).json(posts);
});

// Get single post
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }
  res.status(200).json(post);
});

module.exports = router;
// server.js
const express = require('express');
const path = require('path');
const posts = require('./routes/posts');
const port = process.env.PORT || 8000;

const app = express();

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.use('/api/posts', posts);

app.listen(port, () => console.log(`Server is running on port ${port}`));

Using ES Modules

  • 修改 package.json 檔案,加入 “type”: “module”,
  • 修改 server.js 檔案
  • 修改 posts.js 檔案
// package.json
{
  "name": "express_crash",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node server",
    "dev": "node --watch --env-file=.env server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.19.2"
  }
}
// server.js
import express from 'express';
import path from 'path';
import posts from './routes/posts.js';
const port = process.env.PORT || 8000;

const app = express();

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.use('/api/posts', posts);

app.listen(port, () => console.log(`Server is running on port ${port}`));
// posts.js
import express from 'express';
const router = express.Router();

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
router.get('/', (req, res) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    return res.status(200).json(posts.slice(0, limit));
  }
  res.status(200).json(posts);
});

// Get single post
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }
  res.status(200).json(post);
});

export default router;

Request Body Data

  • 開啟新的 Postman Request
    http://localhost:8000/api/posts
    在 Body > raw 或者 x-www-form-urlencoded
  • 修改 server.js 檔案
  • 修改 posts.js 檔案
  • 使用 x-www-form-urlencoded 輸入 Key: title、Value: My Title
// server.js
import express from 'express';
import path from 'path';
import posts from './routes/posts.js';
const port = process.env.PORT || 8000;

const app = express();

// Body parser middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false}));

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.use('/api/posts', posts);

app.listen(port, () => console.log(`Server is running on port ${port}`));
// posts.js
import express from 'express';
const router = express.Router();

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
router.get('/', (req, res) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    return res.status(200).json(posts.slice(0, limit));
  }
  res.status(200).json(posts);
});

// Get single post
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }
  res.status(200).json(post);
});

// Create new post
router.post('/', (req, res) => {
  console.log(req.body);

  res.status(201).json(posts);
});

export default router;

POST Request

  • 修改 posts.js 檔案
  • 在 API 測試工具輸入 Parameter 1: title、Value 1: Brads Post
// posts.js
import express from 'express';
const router = express.Router();

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
router.get('/', (req, res) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    return res.status(200).json(posts.slice(0, limit));
  }
  res.status(200).json(posts);
});

// Get single post
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }
  res.status(200).json(post);
});

// Create new post
router.post('/', (req, res) => {
  const newPost = {
    id: posts.length + 1,
    title: req.body.title
  };

  if (!newPost.title) {
    return res.status(400).json({ msg: 'Please include a title' });
  }

  posts.push(newPost);
  res.status(201).json(posts);
});

export default router;

PUT Request

  • 修改 posts.js 檔案
  • 使用 API 測試工具、PUT 方法
    http://localhost:8000/api/posts/2
    Key: titl、Value: Updated Post
  • 可以搭配使用 MongoDB 資料庫
// posts.js
import express from 'express';
const router = express.Router();

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
router.get('/', (req, res) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    return res.status(200).json(posts.slice(0, limit));
  }
  res.status(200).json(posts);
});

// Get single post
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }
  res.status(200).json(post);
});

// Create new post
router.post('/', (req, res) => {
  const newPost = {
    id: posts.length + 1,
    title: req.body.title
  };

  if (!newPost.title) {
    return res.status(400).json({ msg: 'Please include a title' });
  }

  posts.push(newPost);
  res.status(201).json(posts);
});

// Update Post
router.put('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }

  post.title = req.body.title;
  res.status(200).json(posts);
});

export default router;

DELETE Request

  • 修改 posts.js 檔案
  • 使用 API 測試工具、DELETE 方法
    http://localhost:8000/api/posts/1
// posts.js
import express from 'express';
const router = express.Router();

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
router.get('/', (req, res) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    return res.status(200).json(posts.slice(0, limit));
  }
  res.status(200).json(posts);
});

// Get single post
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }
  res.status(200).json(post);
});

// Create new post
router.post('/', (req, res) => {
  const newPost = {
    id: posts.length + 1,
    title: req.body.title
  };

  if (!newPost.title) {
    return res.status(400).json({ msg: 'Please include a title' });
  }

  posts.push(newPost);
  res.status(201).json(posts);
});

// Update Post
router.put('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }

  post.title = req.body.title;
  res.status(200).json(posts);
});

// Delete Post
router.delete('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }

  posts = posts.filter((post) => post.id !== id);
  res.status(200).json(posts);
});

export default router;

Middleware

  • 修改 posts.js 檔案
  • 使用 API 測試工具、GET 方法
    http://localhost:8000/api/posts
  • 建立 middleware 資料夾
  • 在 middleware 資料夾裡面建立 logger.js 檔案
  • 修改 server.js 檔案
  • 使用 API 測試工具、GET 方法
    http://localhost:8000/api/posts/1
    http://localhost:8000/api/posts
  • 使用 API 測試工具、DELETE 方法
    http://localhost:8000/api/posts/1
// posts.js
import express from 'express';
const router = express.Router();

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
router.get('/', (req, res) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    return res.status(200).json(posts.slice(0, limit));
  }
  res.status(200).json(posts);
});

// Get single post
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }
  res.status(200).json(post);
});

// Create new post
router.post('/', (req, res) => {
  const newPost = {
    id: posts.length + 1,
    title: req.body.title
  };

  if (!newPost.title) {
    return res.status(400).json({ msg: 'Please include a title' });
  }

  posts.push(newPost);
  res.status(201).json(posts);
});

// Update Post
router.put('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }

  post.title = req.body.title;
  res.status(200).json(posts);
});

// Delete Post
router.delete('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    return res
      .status(404)
      .json({ msg: `A post with the id of ${id} was not found` });
  }

  posts = posts.filter((post) => post.id !== id);
  res.status(200).json(posts);
});

export default router;
// logger.js
const logger = (req, res, next) => {
  console.log(
    `${req.method} ${req.protocol}://${req.get('host')}${req.originalUrl}`
  );
  next();
};

export default logger;
// server.js
import express from 'express';
import path from 'path';
import posts from './routes/posts.js';
import logger from './middleware/logger.js';
const port = process.env.PORT || 8000;

const app = express();

// Body parser middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false}));

// Logger middleware
app.use(logger);

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.use('/api/posts', posts);

app.listen(port, () => console.log(`Server is running on port ${port}`));

Custom Error Handler

  • 使用 API 測試工具、GET 方法
    http://localhost:8000/api/test
  • 修改 posts.js 檔案
  • 在 middleware 資料夾裡面建立 error.js 檔案
  • 修改 server.js 檔案
  • 使用 API 測試工具、GET 方法
    http://localhost:8000/api/posts/100
  • 修改 posts.js 檔案
  • 使用 API 測試工具、POST 方法、未填入 title
    http://localhost:8000/api/posts
    補上 next
// posts.js
import express from 'express';
const router = express.Router();

let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// Get all posts
router.get('/', (req, res, next) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    return res.status(200).json(posts.slice(0, limit));
  }
  res.status(200).json(posts);
});

// Get single post
router.get('/:id', (req, res, next) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    const error = new Error(`A post with the id of ${id} was not found`);
    error.status = 404;
    return next(error);
  }
  res.status(200).json(post);
});

// Create new post
router.post('/', (req, res, next) => {
  const newPost = {
    id: posts.length + 1,
    title: req.body.title
  };

  if (!newPost.title) {
    const error = new Error(`Please include a title`);
    error.status = 400;
    return next(error);
  }

  posts.push(newPost);
  res.status(201).json(posts);
});

// Update Post
router.put('/:id', (req, res, next) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    const error = new Error(`A post with the id of ${id} was not found`);
    error.status = 404;
    return next(error);
  }

  post.title = req.body.title;
  res.status(200).json(posts);
});

// Delete Post
router.delete('/:id', (req, res, next) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    const error = new Error(`A post with the id of ${id} was not found`);
    error.status = 404;
    return next(error);
  }

  posts = posts.filter((post) => post.id !== id);
  res.status(200).json(posts);
});

export default router;
// error.js
const errorHandler = (err, req, res, next) => {
if (err.status) {
  res.status(err.status).json({ msg: err.message });
} else {
  res.status(500).json({ msg: err.message });
}

  res.status(404).json({ msg: err.message });
};

export default errorHandler;
// server.js
import express from 'express';
import path from 'path';
import posts from './routes/posts.js';
import logger from './middleware/logger.js';
import errorHandler from './middleware/error.js';
const port = process.env.PORT || 8000;

const app = express();

// Body parser middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false}));

// Logger middleware
app.use(logger);

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.use('/api/posts', posts);

// Error handler
app.use(errorHandler);

app.listen(port, () => console.log(`Server is running on port ${port}`));

Catch All Error Middleware

  • 使用 API 測試工具、GET 方法
    http://localhost:8000/api/test
  • 修改 server.js 檔案
  • 再次使用 API 測試工具
  • 在 middleware 資料夾裡面建立 notFound.js 檔案
  • 修改 server.js 檔案
  • 再次使用 API 測試工具
// server.js
import express from 'express';
import path from 'path';
import posts from './routes/posts.js';
import logger from './middleware/logger.js';
import errorHandler from './middleware/error.js';
import notFound from './middleware/notFound.js';
const port = process.env.PORT || 8000;

const app = express();

// Body parser middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false}));

// Logger middleware
app.use(logger);

// setup static folder
// app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.use('/api/posts', posts);


// Error handler
app.use(notFound);
app.use(errorHandler);

app.listen(port, () => console.log(`Server is running on port ${port}`));
// notFound.js
const notFound = (req, res, next) => {
  const error = new Error('Not Found');
  error.status = 404;
  next(error);
};

export default notFound;

產生錯誤

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

// error.js - 個人嘗試修正
const errorHandler = (err, req, res, next) => {
  if (err.status) {
    return res.status(err.status).json({ msg: err.message });
  } else {
    return res.status(500).json({ msg: err.message });
  }

  return res.status(404).json({ msg: err.message });
};

export default errorHandler;

Colors Package

  • 安裝 Colors 套件
    npm install colors
  • 修改 logger.js 檔案
  • 使用 API 測試工具、GET 方法
    http://localhost:8000/api/test
  • 再使用 API 測試工具、GET、POST、PUT、DELETE 方法
// logger.js
import colors from 'colors';

const logger = (req, res, next) => {
  const methodColors = {
    GET: 'green',
    POST: 'blue',
    PUT: 'yellow',
    DELETE: 'red'
  };

  const color = methodColors[req.method] || white;
  
  console.log(
    `${req.method} ${req.protocol}://${req.get('host')}${req.originalUrl}`[
      color
    ]
  );
  next();
};

export default logger;

Using Controllers

  • 建立 controllers 資料夾
  • 在 controllers 資料夾裡面建立 postController.js 檔案
  • 修改 posts.js 檔案
  • 修改 postController.js 檔案
  • 使用 API 測試工具、GET 方法
    http://localhost:8000/api/posts/2
    http://localhost:8000/api/posts
    http://localhost:8000/api/posts/43
// controllers/postController.js
let posts = [
  { id: 1, title: 'Post One' },
  { id: 2, title: 'Post Two' },
  { id: 3, title: 'Post Three' },
];

// @desc   Get all posts
// @route  GET /api/posts
export const getPosts = (req, res, next) => {
  const limit = parseInt(req.query.limit);

  if (!isNaN(limit) && limit > 0) {
    return res.status(200).json(posts.slice(0, limit));
  }

  res.status(200).json(posts);
};

// @desc    Get single post
// @route   GET /api/posts/:id
export const getPost = (req, res, next) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    const error = new Error(`A post with the id of ${id} was not found`);
    error.status = 404;
    return next(error);
  }

  res.status(200).json(post);
};

// @desc   Create new post
// @route  POST /api/posts
export const createPost = (req, res, next) => {
  const newPost = {
    id: posts.length + 1,
    title: req.body.title
  };

  if (!newPost.title) {
    const error = new Error(`Please include a title`);
    error.status = 400;
    return next(error);
  }

  posts.push(newPost);
  res.status(201).json(posts);
};

// @desc   Update post
// @route  PUT /api/posts/:id
export const updatePost = (req, res, next) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    const error = new Error(`A post with the id of ${id} was not found`);
    error.status = 404;
    return next(error);
  }

  post.title = req.body.title;
  res.status(200).json(posts);
};

// @desc   Delete post
// @route  DELETE /api/posts/:id
export const deletePost = (req, res, next) => {
  const id = parseInt(req.params.id);
  const post = posts.find((post) => post.id === id);

  if (!post) {
    const error = new Error(`A post with the id of ${id} was not found`);
    error.status = 404;
    return next(error);
  }

  posts = posts.filter((post) => post.id !== id);
  res.status(200).json(posts);
};
// post.js
import express from 'express';
import {
  getPosts,
  getPost,
  createPost,
  updatePost,
  deletePost,
} from '../controllers/postController.js';
const router = express.Router();

// Get all posts
router.get('/', getPosts);

// Get single post
router.get('/:id', getPost);

// Create new post
router.post('/', createPost);

// Update Post
router.put('/:id', updatePost);

// Delete Post
router.delete('/:id', deletePost);

export default router;

__dirname Workaround

  • 修改 server.js 檔案
  • 在瀏覽器網址列輸入
    http://localhost:8000/
    http://localhost:8000/about.html
// server.js
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import posts from './routes/posts.js';
import logger from './middleware/logger.js';
import errorHandler from './middleware/error.js';
import notFound from './middleware/notFound.js';
const port = process.env.PORT || 8000;

// Get the directory name
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();

// Body parser middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// Logger middleware
app.use(logger);

// setup static folder
app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.use('/api/posts', posts);

// Error handler
app.use(notFound);
app.use(errorHandler);

app.listen(port, () => console.log(`Server is running on port ${port}`));

Making Requests From Frontend

  • 修改 public/index.html 檔案
  • 在 public 資料夾下建立 js/main.js 檔案
  • 使用 API 測試工具、POST 方法
    http://localhost:8000/api/posts
    Body > x-www-form-urlencoded 加上 title、New Post
// public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Posts</title>
</head>
<body>
  <h1>Posts</h1>
  <button id="get-posts-btn">Get Posts</button>
  <div id="output"></div>

  <script src="./js/main.js"></script>
</body>
</html>
// public/js/main.js
const output = document.querySelector('#output');
const button = document.querySelector('#get-posts-btn');

// Get and show posts
async function showPosts() {
  try {
    const res = await fetch('http://localhost:8000/api/posts');
    if (!res.ok) {
      throw new Error('Failed to fetch posts');
    }

    const posts = await res.json();
    output.innerHTML = '';

    posts.forEach((post) => {
      const postEl = document.createElement('div');
      postEl.textContent = post.title;
      output.appendChild(postEl);
    })
  } catch (error) {
    console.log('Error fetching posts: ', error);
  }
}

// Event listeners
button.addEventListener('click', showPosts);

Submit Form to API

  • 修改 index.html 檔案
  • 修改 main.js 檔案
  • 使用網頁畫面完成新增 Post 功能
    http://localhost:8000
// public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Posts</title>
</head>
<body>
  <h1>Posts</h1>
  <form id="add-post-form">
    <label for="title">Title:</label><br>
    <input type="text" id="title" name="title"><br>
    <button type="submit">Add Post</button>
  </form>
  <button id="get-posts-btn">Get Posts</button>
  <div id="output"></div>

  <script src="./js/main.js"></script>
</body>
</html>
// public/js/main.js
const output = document.querySelector('#output');
const button = document.querySelector('#get-posts-btn');
const form = document.querySelector('#add-post-form');

// Get and show posts
async function showPosts() {
  try {
    const res = await fetch('http://localhost:8000/api/posts');
    if (!res.ok) {
      throw new Error('Failed to fetch posts');
    }

    const posts = await res.json();
    output.innerHTML = '';

    posts.forEach((post) => {
      const postEl = document.createElement('div');
      postEl.textContent = post.title;
      output.appendChild(postEl);
    })
  } catch (error) {
    console.log('Error fetching posts: ', error);
  }
}

// Submit new post
async function addPost(e) {
  e.preventDefault();
  const formData = new FormData(this);
  const title = formData.get('title');

  try {
    const res = await fetch('http://localhost:8000/api/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({title})
    })

    if (!res.ok) {
      throw new Error('Failed to add post');
    }

    const newPost = await res.json();

    const postEl = document.createElement('div');
    postEl.textContent = newPost.title;
    output.appendChild(postEl);
    showPosts();
  } catch (error) {
    console.error('Error adding post');
  }
}

// Event listeners
button.addEventListener('click', showPosts);
form.addEventListener('submit', addPost);

EJS Template Engine Setup

  • 建立新的專案資料夾 express2
  • 建立 package.json 檔案
    使用終端機 npm init -y
  • 建立 app.js 檔案
  • 安裝 express、ejs 套件
  • 修改 package.json 檔案
  • 建立 views 資料夾
  • 在 views 資料夾裡面建立 index.ejs 檔案
  • 修改 index.ejs 檔案
  • 使用終端機執行指令 npm run dev
    http://localhost:8000/
// package.json
{
  "name": "express2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "node app",
    "dev": "node --watch app"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "ejs": "^3.1.10",
    "express": "^4.19.2"
  }
}
// app.js
import express from 'express';

const app = express();

// Config ejs
app.set('view engine', 'ejs');
app.set('views', 'views');

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

app.listen(8000, () => console.log('Server started'));
// views/index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Welcome</title>
</head>
<body>
  <h1>Welcome</h1>
</body>
</html>

Pass Data to Views

  • 修改 app.js 檔案
  • 修改 index.ejs 檔案
// app.js
import express from 'express';

const app = express();

// Config ejs
app.set('view engine', 'ejs');
app.set('views', 'views');

app.get('/', (req, res) => {
  res.render('index', {
    title: 'My Website',
    message: 'Hello from EJS',
  });
});

app.listen(8000, () => console.log('Server started'));
// index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= title %></title>
</head>
<body>
  <h1><%= message %></h1>
</body>
</html>

Pass and Loop Over Arrays

  • 修改 app.js 檔案
  • 修改 index.ejs 檔案
// app.js
import express from 'express';

const app = express();

// Config ejs
app.set('view engine', 'ejs');
app.set('views', 'views');

app.get('/', (req, res) => {
  res.render('index', {
    title: 'My Website',
    message: 'Hello from EJS',
    people: ['John', 'Jane', 'Jack'],
  });
});

app.listen(8000, () => console.log('Server started'));
// index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= title %></title>
</head>
<body>
  <h1><%= message %></h1>

  <ul>
    <% people.forEach(person => { %>
      <li><%= person %></li>
    <% }); %>
  </ul>
</body>
</html>

Template Partials

  • 在 views 資料夾裡面建立 partials 資料夾
  • 在 partials 資料夾裡面建立 header.ejs 檔案
  • 修改 index.ejs 檔案
// header.ejs
<header>
  <h1>My Website</h1>
</header>
// index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= title %></title>
</head>
<body>
  <%- include('partials/header.ejs') %>
  
  <h1><%= message %></h1>

  <ul>
    <% people.forEach(person => { %>
      <li><%= person %></li>
    <% }); %>
  </ul>
</body>
</html>