พัฒนา Full Stack Web App ด้วย NEXT.js + MySQL deploy ขึ้น Vercel และ TiDB Cloud แบบฟรี!!

Karn Yongsiriwit
9 min readApr 22, 2024

--

เตรียมสภาพแวดล้อมในการพัฒนา

  • ติดตั้ง Node.js (เลือกเป็น LTS version) จากเว็บไซต์ https://nodejs.org/
  • ติดตั้ง Visual Studio Code ใช้เขียนโค้ด https://code.visualstudio.com/
  • ติดตั้งโปรแกรม DBeaver สำหรับใช้จัดการฐานข้อมูล จากเว็บไซต์ https://dbeaver.io/download/
  • สมัครเข้าใช้งาน GitHub https://github.com/
  • สมัครเข้าใช้งาน Vercel https://vercel.com/ แนะนำให้เชื่อมโยงกับ account GitHub
  • ติดตั้ง Git จากเว็บไซต์ https://git-scm.com/
  • รันคำสั่งใน Command Prompt/Terminal เพื่อ config git
git config --global user.name "Firstname Lastname"
git config --global user.email "GitHub Email"

สร้าง Database บน TiDB Cloud

  • เข้าไปที่เว็บไซต์ https://tidbcloud.com/ แล้ว Sign Up ด้วย GitHub
  • ทำการ Sign In แล้วเลือก Cluster0
เลือก Cluster0
  • กดปุ่ม Connect เพื่อดูวิธีการเชื่อมต่อ Database
Connect Database
  • เลือก DBeaver
เลือก DBeaver
  • เปิดโปรแกรม DBeaver แล้วทำการสร้างโปรเจคใหม่
สร้างโปรเจค
ใส่ชื่อโปรเจค
  • สร้าง Connection ในโปรเจค
สร้าง Connection
  • ค้นหาและเลือก TiDB ตามภาพ
เลือก TiDB ใน DBeaver
  • ใส่ Connection URL จาก TiDB Cloud(อาจต้อง Generate/Reset Password)
Connection String
  • ถ้าเชื่อมต่อสำเร็จ จะขึ้นข้อความ Connected
ทดสอบการเชื่อมต่อ
  • กดปุ่ม Finish เพื่อยืนยันและบันทึกการเชื่อมต่อให้สามารถมาใช้ภายหลังได้
สร้าง Connection สำเร็จ
  • ทำการสร้าง Database โดยอาจจะใส่ชื่อเป็น 6090059_dit207
สร้าง Database
ระบุชื่อ Database
  • set ให้ Database ที่สร้างเป็น default
set as default
  • Import ข้อมูลเข้าโดยการสร้าง SQL Script
SQL Script
  • ใช้ SQL ด้านล่าง สำหรับสร้างตาราง และ insert ข้อมูลเข้าตาราง
CREATE TABLE `attractions` (
`id` int PRIMARY KEY AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`detail` varchar(500) NOT NULL,
`coverimage` varchar(100) NOT NULL,
`latitude` decimal(11,7) NOT NULL,
`longitude` decimal(11,7) NOT NULL
);

INSERT INTO `attractions` (`id`, `name`, `detail`, `coverimage`, `latitude`, `longitude`) VALUES
(1, 'Phi Phi Islands', 'Phi Phi Islands are a group of islands in Thailand between the large island of Phuket and the Malacca Coastal Strait of Thailand.', 'https://www.melivecode.com/attractions/1.jpg', '7.7376190', '98.7068755'),
(2, 'Eiffel Tower', 'Eiffel Tower is one of the most famous structures in the world. Eiffel Tower is named after a leading French architect and engineer. It was built as a symbol of the World Fair in 1889.', 'https://www.melivecode.com/attractions/2.jpg', '48.8583736', '2.2922926'),
(3, 'Times Square', 'Times Square has become a global landmark and has become a symbol of New York City. This is a result of Times Square being a modern, futuristic venue, with huge advertising screens dotting its surroundings.', 'https://www.melivecode.com/attractions/3.jpg', '40.7589652', '-73.9893574'),
(4, 'Mount Fuji', 'Mount Fuji is the highest mountain in Japan, about 3,776 meters (12,388 feet) situated to the west of Tokyo. Mount Fuji can be seen from Tokyo on clear days.', 'https://www.melivecode.com/attractions/4.jpg', '35.3606422', '138.7186086'),
(5, 'Big Ben', 'Westminster Palace Clock Tower which is most often referred to as Big Ben. This is actually the nickname for the largest bell that hangs in the vent above the clock face.', 'https://www.melivecode.com/attractions/5.jpg', '51.5007325', '-0.1268141'),
(6, 'Taj Mahal', 'The Taj Mahal or Tachomhal is a burial building made of ivory white marble. The Taj Mahal began to be built in 1632 and was completed in 1643.', 'https://www.melivecode.com/attractions/6.jpg', '27.1751496', '78.0399535'),
(7, 'Stonehenge', 'Stonehenge is a monument prehistoric In the middle of a vast plain in the southern part of the British. The monument itself consists of 112 gigantic stone blocks arranged in 3 overlapping circles.', 'https://www.melivecode.com/attractions/7.jpg', '51.1788853', '-1.8284037'),
(8, 'Statue of Liberty', 'The Statue of Liberty is a colossal neoclassical sculpture on Liberty Island in New York Harbor in New York City, in the United States. The copper statue, a gift from the people of France to the people of the United States.', 'https://www.melivecode.com/attractions/8.jpg', '40.6891670', '-74.0444440'),
(9, 'Sydney Opera House', 'The Sydney Opera House is a multi-venue performing arts centre in Sydney. Located on the banks of the Sydney Harbour, it is often regarded as one of the most famous and distinctive buildings and a masterpiece of 20th century architecture.', 'https://www.melivecode.com/attractions/9.jpg', '-33.8586110', '151.2141670'),
(10, 'Great Pyramid of Giza', 'The Great Pyramid of Giza is the oldest and largest of the pyramids in the Giza pyramid complex bordering present-day Giza in Greater Cairo, Egypt. It is the oldest of the Seven Wonders of the Ancient World, and the only one to remain largely intact.', 'https://www.melivecode.com/attractions/10.jpg', '29.9791670', '31.1341670'),
(11, 'Hollywood Sign', 'The Hollywood Sign is an American landmark and cultural icon overlooking Hollywood, Los Angeles, California. It is situated on Mount Lee, in the Beachwood Canyon area of the Santa Monica Mountains. Spelling out the word Hollywood in 45 ft (13.7 m)-tall white capital letters and 350 feet (106.7 m) long.', 'https://www.melivecode.com/attractions/11.jpg', '34.1340610', '-118.3215920'),
(12, 'Wat Phra Kaew', 'Wat Phra Kaew, commonly known in English as the Temple of the Emerald Buddha and officially as Wat Phra Si Rattana Satsadaram, is regarded as the most sacred Buddhist temple in Thailand. The complex consists of a number of buildings within the precincts of the Grand Palace in the historical centre of Bangkok.', 'https://www.melivecode.com/attractions/12.jpg', '13.7513890', '100.4925000');
  • เลือก Execute SQL
Execute SQL
  • ดูผลลัพธ์ของการสร้างตาราง attractions ตามภาพ
ตาราง attractions

สร้างโปรเจค Full Stack Web App

  • สร้างโปรเจค NEXT.js ด้วยคำสั่งบน Command Prompt/Terminal ตามด้านล่าง
npx create-next-app@latest
  • ระบุรายละเอียดของโปรเจคสำหรับสร้างโปรเจค
ระบุรายละเอียดโปรเจค
  • เข้าไปที่ folder (ชื่อเดียวกับโปรเจค) และเรียก VS Code เพื่อเปิด folder ดังกล่าว
เปิด VS Code
  • ใน VS Code เปิด terminal แล้วใช้คำสั่งด้านล่างเพื่อติดตั้ง package
npm install mysql2
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/icons-material
  • รันคำสั่ง npm run dev แล้วเปิด http://localhost:3000 เพื่อดูผลลัพธ์ของโปรเจค
npm run dev
npm run dev
หน้าเว็บตั้งต้นที่เป็นผลลัพธ์

เตรียม .env ใน NEXT.js

  • ในโปรเจค สร้างไฟล์ .env ไว้นอกสุด เพื่อระบุการเชื่อมต่อ Database และ URL ของ API โดยเปลี่ยน username, password, hostname และ ชื่อ database ตามที่สร้างไว้ใน TiDB Cloud
    .env
MYSQL_URI=mysql://username:password@hostname:4000/6090059_dit207?ssl={"rejectUnauthorized":true}
NEXT_PUBLIC_API_URL=http://localhost:3000
  • โดยค่าของ MYSQL_URI ให้ดูจาก TiDB Cloud หรือจาก DBeaver เพื่อดู Connection URL
DBeaver
  • ทำการ copy Connection URL ดังตัวอย่าง
    mysql://username:password@hostname:4000
    แล้วต่อด้วย
    /database_name?ssl={“rejectUnauthorized”:true}
Connection URL

พัฒนา Back-end (API) ในโปรเจค NEXT.js

  • utils/db.js
const mysql = require('mysql2')
export const mysqlPool = mysql.createPool(process.env.MYSQL_URI)
  • app/api/attractions/route.js
import { NextResponse } from "next/server";
import { mysqlPool } from "@/utils/db";

export async function GET(request) {
const promisePool = mysqlPool.promise()
const [rows, fields] = await promisePool.query(
`SELECT * FROM attractions;`
)
return NextResponse.json(rows)
}
http://localhost:3000/api/attractions
  • app/api/attractions/[id]/route.js
import { NextResponse } from "next/server";
import { mysqlPool } from "@/utils/db";

export async function GET(request, { params }) {
const id = params.id
const promisePool = mysqlPool.promise()
const [rows, fields] = await promisePool.query(
`SELECT * FROM attractions WHERE id = ?`,
[id]
)
return NextResponse.json(rows)
}
  • ทดสอบ API ผ่าน http://localhost:3000/api/attractions/1 (เลข 1 ตอนท้าย URL สามารถเลือกได้ตั้งแต่ 1–12 ตามคอลัมน์ id ของข้อมูลที่มี)
http://localhost:3000/api/attractions/1

พัฒนา Front-End ในโปรเจค NEXT.js

  • app/attractions/page.js
import React from 'react'
import {
Card, CardActions, CardContent, CardMedia, Button, Typography, Grid
} from '@mui/material'

export async function getData() {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/attractions`)
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}

export default async function page() {
if (!process.env.NEXT_PUBLIC_API_URL) {
return null
}
const data = await getData()
return (
<div>
<Typography variant='h5'>Attractions</Typography>
<Grid container spacing={1}>
{data.map(attraction => (
<Grid item key={attraction.id} xs={12} md={4}>
<Card>
<CardMedia
sx={{ height: 140 }}
image={attraction.coverimage}
title={attraction.name}
/>
<CardContent>
<Typography gutterBottom variant="h6" component="div">
{attraction.name}
</Typography>
<Typography variant="body2" color="text.secondary" noWrap>
{attraction.detail}
</Typography>
</CardContent>
<CardActions>
<a href={`/attractions/${attraction.id}`}>
<Button size="small">Learn More</Button>
</a>
</CardActions>
</Card>
</Grid>
))}
</Grid>
</div>
)
}
  • app/attractions/layout.js
import './attractions.css'
import Link from 'next/link'
import {
AppBar, Box, Toolbar, Typography, Button, IconButton, Container
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';

export default function layout({
children,
}) {
return (
<>
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
<Link href="/attractions" style={{ textDecoration: 'none', color: 'white' }}>
Travel App
</Link>
</Typography>
<Button color="inherit">Login</Button>
</Toolbar>
</AppBar>
</Box>
<Container maxWidth="lg">
{children}
</Container>
</>
)
}
  • app/attractions/attractions.css
* {
margin: 0;
padding: 0;
}
  • app/layout.js
import { Inter } from "next/font/google";
// import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
http://localhost:3000/attractions
  • app/attractions/[id]/page.js
import React from 'react'
import {
Container, Card, CardActions, CardContent, CardMedia, Button, Typography
} from '@mui/material';

export async function getData(id) {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/attractions/${id}/`)
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}

export default async function page({ params }) {
if (!process.env.NEXT_PUBLIC_API_URL) {
return null
}
const id = params.id
const data = await getData(id)
console.log(data.length)
return (
<Container maxWidth="md" sx={{ mt: 2 }}>
{ data.length > 0 &&
<Card>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{data[0].name}
</Typography>
</CardContent>
<CardMedia
sx={{ height: 400 }}
image={data[0].coverimage}
title={data[0].name}
/>
<CardContent>
<Typography variant="body2" color="text.secondary">
{data[0].detail}
</Typography>
</CardContent>
</Card>
}
</Container>
)
}
  • ทดสอบหน้าเว็บ http://localhost:3000/attractions/2 (เลข 2 ตอนท้าย URL สามารถเลือกได้ตั้งแต่ 1–12 ตามคอลัมน์ id ของข้อมูลที่มี)
http://localhost:3000/attractions/2

Publish Project ขึ้น GitHub

  • ในโปรเจค NEXT.js ที่ไฟล์ .gitignore ให้เพิ่ม .env เข้าไป 1 บรรทัด เพื่อไม่ให้ไฟล์ .env ถูกนำขึ้น GitHub
.gitignore
  • ทำการ publish โปรเจคขึ้น GitHub โดยเริ่มจากการ Commit ส่วนที่แก้ไข
commit #1
commit #2
  • เลือก publish
Publish to GitHub
  • เปิด GitHub เพื่อดู source code ที่นำขึ้นไป
Open on GitHub

Publish Project ขึ้น Vercel

  • เข้าที่ https://vercel.com/ แล้ว login ด้วย github
  • สร้างโปรเจค
สร้างโปรเจค
  • Import Git Repository จาก GitHub
Import Git Repository
  • ทำการ deploy โดยให้ระบุ Environment ของ MYSQL_URI โดยใช้ค่าจากไฟล์ .env ได้
  • Deploy สำเร็จ เลือก Deployment
Deployment
  • เลือก Domain หลักเพื่อเปิดหน้าเว็บที่ deploy ขึ้นมา
  • หน้าเว็บจะยังไม่แสดงข้อมูล ให้ copy Production URL นี้ไว้เพื่อ set environment ของ Vercel ต่อไป
  • ไปที่โปรเจค แล้วเลือก Settings > Environment Variables
โปรเจค Vercel
เลือก Settings > Environment Variables
  • ใส่ตัวแปร environment ตามภาพ
  • ทำการ Redeploy
Redeploy
Redeploy Confirmation
  • Deploy สำเร็จจะได้ตามภาพ
https://nextjs-deploy-xxx.vercel.app/attractions
https://nextjs-deploy-xxx.vercel.app/attractions/3

บทความโดย อ.ผศ.ดร.กานต์ ยงศิริวิทย์
วิทยาลัยนวัตกรรมดิจิทัลเทคโนโลยี มหาวิทยาลัยรังสิต
Youtube: https://youtube.com/@melivecode
Facebook: https://facebook.com/melivecodepage
Tiktok: https://tiktok.com/@melivecode
Instagram: https://instagram.com/melivecode
Medium: https://karnyong.medium.com
Twitter: https://twitter.com/melivecode
Discord: https://discord.gg/PcvymzR7HD
Free API: https://www.melivecode.com

--

--

Karn Yongsiriwit

Lecturer at Digital Innovation Technology, Rangsit University