Blitz.js เป็น React Framework ที่ Fork มาจาก Next.js วันนี้เราจะสร้าง Blitz.js To-Do Application ที่เก็บ Tasks ใน Upstash เริ่มเลย!
Setup
ต้องติดตั้ง Blitz.js บนคอมพิวเตอร์ก่อน
NPM:
npm install -g blitz --legacy-peer-deps
Yarn:
yarn global add blitz
สร้าง Blitz.js App ใหม่ด้วย blitz new และ cd เข้า Directory
blitz new blitzjs-todo && cd blitzjs-todo
ติดตั้ง TailwindCSS สำหรับ Styling
blitz install tailwind
ติดตั้ง Upstash JS SDK เพื่อเรียก Upstash API ได้ง่ายขึ้น
NPM:
npm i @upstash/redis
Yarn:
yarn i @upstash/redis
รัน blitz dev ตรวจสอบว่าทุกอย่างทำงานถูกต้อง ลองสร้าง Account และ Sign In ควรจะเห็นแบบนี้:

File Structure ควรเป็นแบบนี้:

คัดลอก UPSTASH_REDIS_REST_URL และ UPSTASH_REDIS_REST_TOKEN จาก Upstash Console ใส่ไฟล์ .env:
# This env file should be checked into source control
# This is the place for default values for all environments
# Values in `.env.local` and `.env.production` will override these values
UPSTASH_REDIS_REST_URL=YOUR_URL_HERE
UPSTASH_REDIS_REST_TOKEN=YOUR_TOKEN_HERE
Setup Blitz.js Application เสร็จแล้ว เริ่ม Implement To-Do List!
Implementation
Blitz.js มาพร้อม User Authentication Built-in! ใช้สำหรับ Private To-Do Lists ต่อ User
เริ่มต้น Upstash JS SDK ใน /lib/redis.ts
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
export default redis;
ต้องทำ 3 API Routes เข้าถึง To-Do Lists
ไปที่ app/api สร้างไฟล์ getall.ts วางโค้ดนี้:
import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";
export const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
const session = await getSession(req, res);
if (!session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` });
} else {
await redis
.lrange(String(session.userId), 0, 100)
.then((data) => res.status(200).json({ data: data, success: true }))
.catch((error) => res.status(500).json({ error: error }));
}
};
export default handler;
API Route นี้ทำงานยังไง: Request เข้า Route Validate User Logged In หากไม่มี User Return "Not Authorized" หากมี User Fetch Upstash Redis หา To-Dos ทั้งหมดใน List (ประมาณ 100 รายการ)
Q: แล้วเราจะ Add To-Dos ยังไง? A: คำถามดี! มาทำต่อ!
วางโค้ดนี้ในไฟล์ใหม่ add.ts ใน app/api:
import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";
const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
const session = await getSession(req, res);
if (req.method !== "POST" || !req.body.data || !session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` });
} else {
let todo = encodeURI(req.body.data);
await redis
.lpush(String(session.userId), todo)
.then(() => res.status(200).json({ success: true }))
.catch(() => res.status(500).json({ error: "Error adding data." }));
}
};
export default handler;
API Route นี้คล้ายก่อนหน้า แต่เพิ่ม Checks ในบรรทัดที่ 5 เพราะเป็น POST Request ตรวจสอบ 3 อย่าง: 1) เป็น POST Request จริง 2) มี JSON/Text ใน req.body.data 3) User Logged In หากผ่านทั้ง 3 Push To-Do ไป Redis List บน Upstash Error ใดๆ Return 500 ด้วย .catch
Route สุดท้าย: Remove To-Dos สร้าง app/api/remove.ts วางโค้ดนี้:
import { BlitzApiRequest, BlitzApiResponse, getSession } from "blitz";
import redis from "../../lib/redis";
const handler = async (req: BlitzApiRequest, res: BlitzApiResponse) => {
const session = await getSession(req, res);
if (req.method !== "POST" || !req.body.data || !session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` });
} else {
let todo = encodeURI(req.body.data);
await redis
.lrem(String(session.userId), 1, todo)
.then(() => res.status(200).json({ success: true }))
.catch(() => res.status(500).json({ error: "Error removing data." }));
}
};
export default handler;
เหมือน add API Route เกือบทุกอย่าง ความต่างคือใช้ LREM แทน LPUSH เพื่อ Remove Item จาก Redis
Building The Frontend
ลบทุกอย่างใน app/pages/index.js เขียน To-Do List ใหม่ทีละขั้นตอน
Imports ด้านบน:
import { Link, BlitzPage, useMutation, Routes, getAntiCSRFToken } from "blitz";
import { useRef, useEffect, useState, Suspense } from "react";
import Layout from "app/core/layouts/Layout";
import { useCurrentUser } from "app/core/hooks/useCurrentUser";
import logout from "app/auth/mutations/logout";
ใช้ React Hooks สร้าง Core Functionality
const Main = () => {
const todoRef = useRef<HTMLInputElement>(null);
const [todos, setTodos] = useState([]);
const currentUser = useCurrentUser();
const [logoutMutation] = useMutation(logout);
const handleAddTodo = async (e) => {
e.preventDefault();
const antiCSRFToken = await getAntiCSRFToken();
const response = await fetch("/api/add", {
method: "POST",
headers: {
"Content-Type": "application/json",
"anti-csrf": antiCSRFToken,
},
body: JSON.stringify({ data: todoRef.current?.value }),
});
const data = await response.json();
if (data.success) {
todoRef.current!.value = "";
fetchTodos();
}
};
const handleRemoveTodo = async (id) => {
const antiCSRFToken = await getAntiCSRFToken();
const response = await fetch("/api/remove", {
method: "POST",
headers: {
"Content-Type": "application/json",
"anti-csrf": antiCSRFToken,
},
body: JSON.stringify({ data: id }),
});
const data = await response.json();
if (data.success) {
fetchTodos();
}
};
const fetchTodos = async () => {
const antiCSRFToken = await getAntiCSRFToken();
const response = await fetch("/api/getall", {
method: "GET",
headers: { "anti-csrf": antiCSRFToken },
});
const res = await response.json();
setTodos(res.data);
};
useEffect(() => { fetchTodos(); }, []);
if (currentUser) {
return (
<>
<button className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400 mb-3"
onClick={async () => { await logoutMutation(); }}>
Logout
</button>
<div> User id: <code>{currentUser.id}</code> <br />
User email: <code>{currentUser.email}</code> </div>
<form className="mt-2" onSubmit={handleAddTodo}>
<p>add a todo:</p>
<input ref={todoRef} className="w-full border-black border-2 focus:outline-none text-center" />
</form>
<div className="flex flex-col gap-2 mt-4 bg-gray-300 rounded-md">
{(todos as string[]).map((todo: string, index: number) => (
<div className="flex items-center p-3 rounded-md bg-gray-300" key={index}>
<button onClick={() => handleRemoveTodo(todo)}
className="flex items-center mr-4 justify-center w-5 h-5 rounded-[0.25rem] border border-solid border-gray-500 shadow-sm hover:bg-gray-700">
</button>
<span>{todo}</span>
</div>
))}
</div>
</>
);
} else {
return (
<div className="flex flex-col gap-4 text-center">
<Link href={Routes.SignupPage()}>
<a className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400">
<strong>Sign Up</strong> </a> </Link>
<Link href={Routes.LoginPage()}>
<a className="mt-4 px-2 py-1 border-2 border-black hover:bg-gray-400">
<strong>Login</strong> </a> </Link>
</div>
);
}
};
Component <Main/> เป็นหัวใจของ Application ใช้ State และ Ref สำหรับ Input "New To-DO" ใช้ antiCSRFToken Blitz.js ต้องการ Tokens นี้เมื่อ Fetch API Routes ป้องกัน Malicious Actors
3 Functions หลักจัดการ Data:
handleAddTodohandleRemoveTodofetchTodos
เรียก fetchTodos ตอนโหลดหน้า เพื่อ Load To-Dos ที่ User ยังต้องทำ เมื่อ User Add/Remove To-Do เรียก fetchTodos อีกครั้งเพื่อ Reflect Changes
หาก User ไม่ได้ Login จะถูก Prompt ให้ Login ก่อนเห็นหน้านี้
Sign Up หรือ Log In หากยังไม่มี Session จำ To-DOs ไม่ได้โดยไม่มี Account และทุก API Routes ต้อง Authentication ด้วย AntiCSRFToken!
Export Page:
const Home: BlitzPage = () => {
return (
<div className="flex flex-col min-h-screen items-center justify-center">
<main>
<div className="my-4">
<Suspense fallback="Loading..."> <Main /> </Suspense>
</div>
</main>
</div>
);
};
Home.suppressFirstRenderFlicker = true;
Home.getLayout = (page) => <Layout title="Home">{page}</Layout>;
export default Home;
Blitz.js ใช้ Approach ต่าง Next.js เล็กน้อย แต่ Core เหมือนกัน ใช้ Suspense แสดง Loading แล้วค่อยแสดง <Main/>
รัน blitz dev อีกครั้ง เปิด Browser ดู App
ถ้าทำตามคำแนะนำ App ควรประมาณนี้หลัง Login และ Add To-DOs:

Remove To-Do ได้โดยคลิก Box ข้างๆ นั่นคือ removeTodo Function
ยินดีด้วย!
หวังว่าเรียนรู้อะไรใหม่จากบล็อกนี้ หากไม่ก็ถือว่า Brush Up Skills แล้ว Blitz.js กำลัง Pivot Focus ออกจาก Next.js อาจเป็น Framework ต่างหากในอนาคต ติดตามเว็บไซต์ ของพวกเขา!
Project Source: GitHub Link
Working Demo: Demo Link
มี Feedback? Follow @upstash on Twitter และ Join Discord Server!