ในบทความนี้ เราจะสร้าง Todo Application แบบ Serverless Next.js เราจะพยายามทำให้ Minimalist ที่สุด จะไม่มี Database Connection ไม่มี Dependency เพิ่มเติมนอกจาก Next.js ไม่มีปุ่มกด เพราะ minimalism ดูทันสมัยและสะอาดตา ผมชอบเพราะผมเป็นนักพัฒนาขี้เกียจ :)
ทำไมเราจึงหลีกเลี่ยง Database Connections
Next.js เป็นเฟรมเวิร์กสมัยใหม่ที่ช่วยให้นักพัฒนา Front-end พัฒนา Full Stack ได้ ฟังก์ชัน Serverless มีบทบาทสำคัญในการลดความซับซ้อนของ Backend สำหรับนักพัฒนา Next.js ตามที่คุณรู้ ฟังก์ชัน Serverless ไม่ค่อยได้กับ Database Connections เนื่องจากธรรมชาติ Stateless ดู ที่นี่ และ ที่นี่ เป็นตัวอย่างปัญหาของ Database Connections ภายใน Serverless Functions
REST คือคำตอบ
REST อนุญาตให้ Client และ Server สื่อสารโดยไม่ต้องมี Session Information ความ Stateless และความเรียบง่ายทำให้ REST เป็นโปรโตคอลการสื่อสารที่สมบูรณ์แบบสำหรับสภาพแวดล้อม Serverless เราจะเข้าถึง Upstash Redis ด้วย REST
Tech Stack ของโปรเจกต์
- Frontend: Next.js
- Backend: Vercel Functions
- Database: Upstash Redis ด้วย REST API
ดู Demo: https://nextjs-todo-zeta.vercel.app/
ดูโค้ด: GitHub Repository
การตั้งค่าโปรเจกต์
สร้าง Next.js App: npx create-next-app
สร้าง Upstash Redis Database ในภูมิภาค AWS-US-EAST-1 และคัดลอก REST URL และ Token
โปรเจกต์จะเป็น Single Page Application พร้อม 3 API Endpoints:
- pages/api/list.js: แสดงรายการ Todo Items
- pages/api/add.js: เพิ่ม Todo Item
- pages/api/remove.js: ลบ Todo Item
โค้ด
เพิ่ม pages/api/list.js ดังนี้:
export default async (req, res) => {
const token = "REPLACE_YOUR_TOKEN";
const url = "https://REPLACE_YOUR_ENDPOINT/lrange/todo/0/100?_token=" + token;
return fetch(url)
.then((r) => r.json())
.then((data) => {
let result = JSON.stringify(data.result);
return res.status(200).json(result);
});
};
เพิ่ม pages/api/add.js ดังนี้:
export default async (req, res) => {
if (!req.query.todo) {
return res.status(400).send("todo parameter required.");
}
let todo = encodeURI(req.query.todo);
const token = "REPLACE_YOUR_TOKEN";
const url = "https://REPLACE_YOUR_ENDPOINT/lpush/todo/" + todo + "?_token=" + token;
return fetch(url)
.then((r) => r.json())
.then((data) => {
let result = JSON.stringify(data.result);
return res.status(200).json(result);
});
};
เพิ่ม pages/api/remove.js ดังนี้:
export default async (req, res) => {
if (!req.query.todo) {
return res.status(400).send("todo parameter required.");
}
let todo = encodeURI(req.query.todo);
const token = "REPLACE_YOUR_TOKEN";
const url = "https://REPLACE_YOUR_ENDPOINT/lrem/todo/1/" + todo + "?_token=" + token;
return fetch(url)
.then((r) => r.json())
.then((data) => {
let result = JSON.stringify(data.result);
return res.status(200).json(result);
});
};
อัปเดต pages/index.js ดังนี้:
import Head from "next/head";
import Image from "next/image";
import styles from "../styles/Home.module.css";
import { useEffect, useState } from "react";
export default function Home() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [todo, setTodo] = useState("");
let changeHandler = (event) => {
setTodo(event.target.value);
};
let addTodo = (event) => {
setLoading(true);
event.preventDefault();
fetch("/api/add?todo=" + todo)
.then((res) => res.json())
.then((data) => {
loadTodos();
});
};
let removeTodo = (rtodo) => {
setLoading(true);
fetch("/api/remove?todo=" + rtodo)
.then((res) => res.json())
.then((data) => {
loadTodos();
});
};
let loadTodos = () => {
console.log("load todos");
fetch("/api/list")
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
});
};
useEffect(() => {
setLoading(true);
loadTodos();
}, []);
if (!data) return "Loading...";
return (
<div className={styles.container}>
<Head>
<title>Next.js TODO APP</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div className={styles.grid}>
<h1 className={styles.title}>
TODO App with{" "}
<a href="https://blog.upstash.com/nextjs-todo">Next.js!</a>
<br />
<br />
</h1>
{loading ? (
<a href="#" className={styles.card}>
<img src="/loader.gif" />
</a>
) : (
<form className={styles.cardForm} onSubmit={addTodo}>
<input
className={styles.cardInput}
type="text"
name="todo"
onChange={changeHandler}
placeholder="Enter your exciting TODO item!"
/>
</form>
)}
{data.map((item) => (
<a href="#" onClick={() => removeTodo(item)} className={styles.card}>
<p>{item}</p>
</a>
))}
</div>
</main>
<footer className={styles.footer}>
<a href="https://blog.upstash.com/nextjs-todo" target="_blank" rel="noopener noreferrer">
Powered by{" "}
<span className={styles.logo}>
<Image src="/logo.png" alt="Upstash Logo" width={87} height={25} />
</span>
</a>
</footer>
</div>
);
}
ตามที่เห็น นี่คือแอป React พื้นฐานที่ใช้ Hooks มี 3 เมธอดที่โต้ตอบกับ API: addTodo, removeTodo และ loadTodos
สุดท้าย อัปเดตไฟล์ styles/Home.module.css ตาม ที่นี่
รันและ Deploy
รันโปรเจกต์ในเครื่องด้วย npm run dev หากทุกอย่างดูดี คุณสามารถ Deploy ได้โดยรัน vercel ในโฟลเดอร์โปรเจกต์ Vercel จะสร้าง Serverless Functions สำหรับ API Functions ของคุณ ภูมิภาคเริ่มต้นสำหรับ Vercel Functions คือ US-EAST-1 นั่นคือเหตุผลที่เราสร้างฐานข้อมูลในภูมิภาคเดียวกัน
หมายเหตุ
- ปลอดภัยกว่าที่จะเก็บ Database Token ใน Vercel Environment Variable
- เหมาะสำหรับประสิทธิภาพที่จะเก็บ Serverless Function และ Redis Database ไว้ในภูมิภาคเดียวกัน
- เราอาจใช้ Redis Clients แทน REST API แต่เช่นที่กล่าวมา Database Connections อาจก่อให้เกิดปัญหาใน Serverless Functions ยังไงก็ตาม เราไม่เห็นความแตกต่างของประสิทธิภาพมากนักระหว่าง Upstash REST API และ Native API