😀Building a Simple CRUD API with Next.js 13 Full (ok)
https://hoanguyenit.com/building-a-simple-crud-api-with-nextjs-13.html
Last updated
Was this helpful?
https://hoanguyenit.com/building-a-simple-crud-api-with-nextjs-13.html
Last updated
Was this helpful?
package.json
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "nodemon src/index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@types/express": "^5.0.0",
"@types/mysql": "^2.15.26",
"@types/node": "^22.10.7",
"@types/typescript": "^2.0.0",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"mysql2": "^3.12.0",
"nodemon": "^3.1.9",
"ts-node": "^10.9.2",
"typescript": "^5.7.3"
}
}
.env
PORT=8000
src\index.ts
import userRoute from './routes/userRoute';
import postRoute from './routes/postRoute';
const express = require('express');
const dotenv = require('dotenv');
dotenv.config();
const app = express();
const port = process.env.PORT;
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get("/", (req: any, res: any) => {
res.sendFile('./index.html', {
root: __dirname
});
});
app.use("/api/users", userRoute);
app.use("/api/posts", postRoute);
app.listen(port, () => {
console.log(`[server]: Server is running at http://localhost:${port}`);
});
src\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<h1>Home</h1>
<a href="/api/users">Users</a>
<a href="/api/posts">Posts</a>
</body>
</html>
src\helper.ts
export function getOffset(currentPage = 1, listPerPage: any) {
return (currentPage - 1) * <any>[listPerPage];
}
export function emptyOrRows(rows: any) {
if (!rows) {
return [];
}
return rows;
}
src\config.ts
const config = {
/* don't expose password or any sensitive info, done only for demo */
host: "localhost",
user: "root",
password: "",
database: "test",
listPerPage: 10,
};
export default config;
src\services\usersServic.ts
import query from "./db";
import * as helper from '../helper';
import config from "../config";
export async function getMultiple(page = 1) {
const offset = helper.getOffset(page, config.listPerPage);
const rows = await query(
`SELECT id, name FROM users LIMIT ${offset},${config.listPerPage}`
);
const data = helper.emptyOrRows(rows);
const meta = { page };
return {
data,
meta
}
}
export async function create(user: any) {
console.log(user);
const result: any = await query(
`INSERT INTO users (name) VALUES ('${user.name}');`
);
let message = 'Error in creating user';
if (result.affectedRows) {
message = 'user created successfully';
}
return {
message
};
}
export async function update(id: number, user: any) {
const result: any = await query(
`UPDATE users SET name="${user.name}" WHERE id=${id}`
);
let message = 'Error in updating user';
if (result.affectedRows) {
message = 'user updated successfully';
}
return {
message
};
}
export async function remove(id: number) {
const result: any = await query(
`DELETE FROM users WHERE id=${id}`
);
let message = 'Error in deleting user';
if (result.affectedRows) {
message = 'user deleted successfully';
}
return { message };
}
src\services\postsServic.ts
import query from "./db";
import * as helper from '../helper';
import config from "../config";
export async function getMultiple(page = 1) {
const offset = helper.getOffset(page, config.listPerPage);
const rows = await query(
`SELECT id, title,content,publish,created_at FROM posts LIMIT ${offset},${config.listPerPage}`
);
const data = helper.emptyOrRows(rows);
const meta = { page };
return {
data,
meta
}
}
export async function getOne(id = 1) {
const rows = await query(
`SELECT id, title,content,publish,created_at FROM posts WHERE id = ${id}`
);
const data = helper.emptyOrRows(rows);
return data[0]
}
export async function create(posts: any) {
const result: any = await query(
`INSERT INTO posts (title,content) VALUES ('${posts.title}','${posts.content}');`
);
let message = 'Error in creating posts';
if (result.affectedRows) {
message = 'posts created successfully';
}
return {
message
};
}
export async function update(id: number, posts: any) {
const result: any = await query(
`UPDATE posts SET title='${posts.title}',content='${posts.content}' WHERE id=${id}`
);
let message = 'Error in updating posts';
if (result.affectedRows) {
message = 'posts updated successfully';
}
return {
message
};
}
export async function remove(id: number) {
const result: any = await query(
`DELETE FROM posts WHERE id=${id}`
);
let message = 'Error in deleting posts';
if (result.affectedRows) {
message = 'posts deleted successfully';
}
return { message };
}
src\services\db.ts
import mysql from 'mysql2/promise';
import config from "../config";
async function query(sql: any, params: any = []) {
const connection = await mysql.createConnection({
host: config.host,
user: config.user,
password: config.password,
database: config.database
});
const [results] = await connection.execute(sql, params);
return results;
}
export default query;
src\routes\userRoute.ts
import * as userRoute from '../services/usersServic';
const express = require('express');
const router = express.Router();
router.get('/', async function (req: any, res: any, next: any) {
try {
res.json(await userRoute.getMultiple(req.query.page));
} catch (err: any) {
console.error(`Error while getting users `, err.message);
next(err);
}
});
router.post("/", async function (req: any, res: any, next: any) {
try {
res.json(await userRoute.create(req.body));
} catch (err: any) {
console.error(`Error while getting users `, err.message);
next(err);
}
});
export default router;
router.put('/:id', async function (req: any, res: any, next: any) {
try {
res.json(await userRoute.update(req.params.id, req.body));
} catch (err: any) {
console.error(`Error while updating user`, err.message);
next(err);
}
});
router.delete('/:id', async function (req: any, res: any, next: any) {
try {
res.json(await userRoute.remove(req.params.id));
} catch (err: any) {
console.error(`Error while deleting user`, err.message);
next(err);
}
});
src\routes\postRoute.ts
import * as postRoute from '../services/postsServic';
const express = require('express');
const router = express.Router();
router.get('/', async function (req: any, res: any, next: any) {
try {
res.json(await postRoute.getMultiple(req.query.page));
} catch (err: any) {
console.error(`Error while getting users `, err.message);
next(err);
}
});
router.get('/:id', async function (req: any, res: any, next: any) {
try {
res.json(await postRoute.getOne(req.params.id));
} catch (err: any) {
console.error(`Error while getting users `, err.message);
next(err);
}
});
router.post("/", async function (req: any, res: any, next: any) {
try {
res.json(await postRoute.create(req.body));
} catch (err: any) {
console.error(`Error while getting users `, err.message);
next(err);
}
});
export default router;
router.put('/:id', async function (req: any, res: any, next: any) {
try {
res.json(await postRoute.update(req.params.id, req.body));
} catch (err: any) {
console.error(`Error while updating user`, err.message);
next(err);
}
});
router.delete('/:id', async function (req: any, res: any, next: any) {
try {
res.json(await postRoute.remove(req.params.id));
} catch (err: any) {
console.error(`Error while deleting user`, err.message);
next(err);
}
});
package.json
{
"name": "next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "15.1.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"swr": "^2.3.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
.env
PATH_URL_BACKEND=http://localhost:8000
app\page.tsx
import Posts from './post/page'
export default function Home() {
return <Posts />
}
app\layout.tsx
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import Header from './components/Header'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<Header />
<div className='w-full max-w-7xl mt-4 m-auto'>
{children}
</div>
</body>
</html>
)
}
app\types\index.ts
export interface UserModel {
id: number,
name: string,
}
export interface PostModel {
id: number,
title: string,
keyword: string,
des: string,
slug: string,
image: string,
publish: number,
content: string,
created_at: string
user: UserModel,
deletePost: (id: number) => void;
}
export interface PostAddModel {
title: string,
content: string
}
app\post\page.tsx
"use client";
import React, { useEffect, useState } from "react";
import useSWR from "swr";
import { fetcher } from "../libs";
import Post from "../components/Post";
import { PostModel } from "../types";
import Link from "next/link";
export default function Posts() {
const [posts, setPosts] = useState<PostModel[]>([]);
const { data, error, isLoading } = useSWR<any>(`/api/posts`, fetcher);
useEffect(() => {
if (data && data.result.data) {
setPosts(data.result.data);
}
}, [data, isLoading]);
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return null;
let delete_Post: PostModel['deletePost'] = async (id: number) => {
const res = await fetch(`/api/posts/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
});
const content = await res.json();
if (content.success > 0) {
setPosts(posts?.filter((post: PostModel) => { return post.id !== id }));
}
}
return (
<div className="w-full max-w-7xl m-auto">
<table className="w-full border-collapse border border-slate-400">
<caption className="caption-top py-5 font-bold text-green-500 text-2xl">
List Posts - Counter :
<span className="text-red-500 font-bold">{posts?.length}</span>
</caption>
<thead>
<tr className="text-center">
<th className="border border-slate-300">ID</th>
<th className="border border-slate-300">Title</th>
<th className="border border-slate-300">Hide</th>
<th className="border border-slate-300">Created at</th>
<th className="border border-slate-300">Modify</th>
</tr>
</thead>
<tbody>
<tr>
<td colSpan={5}>
<Link href={`/post/create`} className="bg-green-500 p-2 inline-block text-white">Create</Link>
</td>
</tr>
{
posts && posts.map((item: PostModel) => <Post key={item.id} {...item} deletePost={delete_Post} />)
}
</tbody>
</table>
</div>
);
}
app\post\read[id]\page.tsx
'use client'
import { fetcher } from '@/app/libs'
import useSWR from 'swr';
import { useParams } from 'next/navigation';
export default function Detail() {
const params = useParams<{ id: string}>();
const { id } = params;
const { data: post, isLoading, error } = useSWR(`/api/posts/${id}`, fetcher);
if (isLoading) return <div><span>Loading...</span></div>
if (!post) return null;
return (
<div className='w-full'>
<h2 className='font-bold text-3xl py-3'>{post.title}</h2>
<div className='w-full max-w-4xl'>
<p dangerouslySetInnerHTML={{ __html: post.content }}></p>
</div>
</div>
)
}
app\post\edit[id]\page.tsx
"use client"
import React, { useState, useEffect } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { fetcher } from '@/app/libs'
import useSWR from 'swr';
export default function PostEdit() {
const router = useRouter();
const params = useParams<{ id: string }>();
const { id } = params;
const { data: post, isLoading, error } = useSWR(`/api/posts/${id}`, fetcher);
const [title, setTitle] = useState<string>('');
const [body, setBody] = useState<string>('');
useEffect(() => {
if (post) {
setTitle(post.title)
setBody(post.content)
}
}, [post, isLoading])
const updatePost = async (e: any) => {
e.preventDefault()
if (title != "" && body != "") {
const formData = {
title: title,
content: body
}
const res = await fetch(`/api/posts/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const content = await res.json();
if (content.success > 0) {
router.push('/post');
}
}
};
if (isLoading) return <div><span>Loading...</span></div>
if (!post) return null;
return (
<form className='w-full' onSubmit={updatePost}>
<span className='font-bold text-yellow-500 py-2 block underline text-2xl'>Form Add</span>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Title</label>
<input type='text' name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' value={title} onChange={(e: any) => setTitle(e.target.value)} />
</div>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Content</label>
<textarea name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' value={body} onChange={(e: any) => setBody(e.target.value)} />
</div>
<div className='w-full py-2'>
<button className="w-20 p-2 text-white border-gray-200 border-[1px] rounded-sm bg-green-400">Submit</button>
</div>
</form>
)
}
app\post\create\page.tsx
"use client"
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
export default function PostCreate() {
const router = useRouter()
const [title, setTitle] = useState<string>('');
const [body, setBody] = useState<string>('');
const addPost = async (e: any) => {
e.preventDefault()
if (title != "" && body != "") {
const formData = {
title: title,
content: body
}
const add = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const content = await add.json();
if (content.success > 0) {
router.push('/post');
}
}
};
return (
<form className='w-full' onSubmit={addPost}>
<span className='font-bold text-yellow-500 py-2 block underline text-2xl'>Form Add</span>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Title</label>
<input type='text' name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' onChange={(e: any) => setTitle(e.target.value)} />
</div>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Content</label>
<textarea name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' onChange={(e: any) => setBody(e.target.value)} />
</div>
<div className='w-full py-2'>
<button className="w-20 p-2 text-white border-gray-200 border-[1px] rounded-sm bg-green-400">Submit</button>
</div>
</form>
)
}
app\libs\index.ts
export const fetcher = (url: string) => fetch(url).then((res) => res.json());
app\components\Header.tsx
import Link from 'next/link'
import React from 'react'
export default function Header() {
return (
<div className='w-full bg-gray-300 py-4 px-3'>
<Link href={"/"}>Home</Link>
</div>
)
}
app\components\Post.tsx
import React from 'react'
import { PostModel } from '../types'
import Link from 'next/link'
export default function Post(params: PostModel) {
return (
<tr>
<td className='w-10 border border-slate-300 text-center' > {params.id} </td>
<td className='border border-slate-300' > {params.title} </td>
<td className='border border-slate-300 text-center' > {params.publish > 0 ? 'open' : 'hide'} </td>
<td className='border border-slate-300 text-center' > {params.created_at} </td>
<td className='w-52 border border-slate-300'>
<span onClick={() => params.deletePost(params.id)} className='bg-red-500 p-2 inline-block text-white text-sm' > Delete </span>
<Link href={`/post/edit/${params.id}`} className='bg-yellow-500 p-2 inline-block ml-3 text-white text-sm' > Edit</Link>
<Link href={`/post/read/${params.id}`} className='bg-yellow-500 p-2 inline-block ml-3 text-white text-sm' > View </Link>
</td>
</tr>
)
}
app\api\posts\route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET() {
const res = await fetch(process.env.PATH_URL_BACKEND + '/api/posts', {
headers: {
'Content-Type': 'application/json',
},
})
const result = await res.json()
return NextResponse.json({ result })
}
export async function POST(request: NextRequest) {
const body = await request.json()
const res = await fetch(process.env.PATH_URL_BACKEND + '/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
const data = await res.json();
return NextResponse.json(data)
}
app\api\posts[id]\route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest, { params }: { params: { id: number } }) {
const res = await fetch(process.env.PATH_URL_BACKEND + `/api/posts/${params.id}`, {
next: { revalidate: 10 },
headers: {
'Content-Type': 'application/json',
},
})
const result = await res.json()
return NextResponse.json(result)
}
export async function PUT(request: NextRequest, { params }: { params: { id: number } }) {
const body = await request.json()
const res = await fetch(process.env.PATH_URL_BACKEND + `/api/posts/${params.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
const data = await res.json();
return NextResponse.json(data)
}
export async function DELETE(request: NextRequest, { params }: { params: { id: number } }) {
const res = await fetch(process.env.PATH_URL_BACKEND + `/api/posts/${params.id}`, {
next: { revalidate: 10 },
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
const data = await res.json();
return NextResponse.json(data)
}
Nay mình sẽ làm một ví dụ về CRUD(create, read, update, delete) trong NextJS 13. Chia sẻ với mọi người cách thiết lập route trong NextJS 13 này, để ta có thể cấu hình các đường truyền đến create, read, edit trong ứng dụng. Ở đây mình dùng phiên bản NextJS 13 mới nhất. Với mình đã có sẵn BackEnd rồi, nên trong bài này chỉ làm frontend thôi
+ app/libs/index.ts : xây dựng các thư viện bạn muốn + app/types/index.ts : xây dựng các interface + api/posts/route.ts : GET (Lấy danh sách tất cả bài viết), POST(Thêm một bài viết) + api/posts/[id]/route.ts : GET : lấy bài viết qua ID PUT : cập nhật bài viết từ ID DELETE: xóa bài viết từ ID + app/post/page.tsx : Hiện thị danh sách bài viết + app/post/create/page.tsx : Form thêm bài viết + app/post/edit/[id]/page.tsx : Form chỉnh sửa bài viết từ ID + app/post/read/[id]/page.tsx : Form hiển thị bài viết từ ID + app/components/Header.ts : thiết kế giao diện phần header + app/components/Post.ts : hiện thị dữ liệu bài post + app/layout.tsx : giao diện bố cục của project + app/page.tsx : giao diện trang chủ
Github : Building A Simple CRUD API With Next.Js 13
Okay bắt đầu chúng ta xây dựng một project thôi
npx create-next-app@latest
Bạn nào chưa xem bài viết tạo project NextJS thì xem lại bài viết này : Create A Project With Next.Js
+ app/libs/index.ts : Đoạn code bên dưới, dúng ta xử lý request API
export const fetcher = (url: string) => fetch(url).then((res) => res.json());
+ app/types/index.ts : thiết lập các thuộc tính của một Model, bằng cách sử dụng interface trong typescript , cần cấu hình các thuộc tính thuộc một kiểu dữ liệu nào đó
export interface UserModel{
id:number,
name:string,
}
export interface PostModel{
id:number,
title:string,
keyword:string,
des:string,
slug:string,
image:string,
publish:number,
content:string,
created_at:string
user:UserModel,
deletePost:(id: number)=> void;
}
export interface PostAddModel{
title:string,
content:string
}
+ api/posts/route.ts : Chúng ta cần xây dụng một route, để request Api , ở đây ta cần cài đặt 2 phương thức(GET, POST)
import { NextRequest, NextResponse } from 'next/server';
export async function GET() {
const res = await fetch(process.env.PATH_URL_BACKEND+'/api/posts', {
headers: {
'Content-Type': 'application/json',
},
})
const result = await res.json()
return NextResponse.json({ result })
}
export async function POST(request: NextRequest) {
const body = await request.json()
const res = await fetch(process.env.PATH_URL_BACKEND+'/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
const data = await res.json();
return NextResponse.json(data)
}
process.env.PATH_URL_BACKEND : là đường dẫn địa chỉ BackEnd của bạn, các bạn tạo file .env và sử dụng các biến cấu hình cho project nhé
+ api/posts/[id]/route.ts : Trong route này ta sử dụng các phương thức như (GET, PUT, DELETE), cũng như mình đã nói ở phần bên trên GET : dùng để lấy bài viết theo ID PUT : cập nhật bài viết từ ID DELETE : xóa bài viết từ ID
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request : NextRequest,{ params }: { params: { id: number } }) {
const res = await fetch(process.env.PATH_URL_BACKEND+`/api/posts/${params.id}`, {
next: { revalidate: 10 } ,
headers: {
'Content-Type': 'application/json',
},
})
const result = await res.json()
return NextResponse.json(result)
}
export async function PUT(request: NextRequest,{ params }: { params: { id: number } }) {
const body = await request.json()
const res = await fetch(process.env.PATH_URL_BACKEND+`/api/posts/${params.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
const data = await res.json();
return NextResponse.json(data)
}
export async function DELETE(request: NextRequest,{ params }: { params: { id: number } }) {
const res = await fetch(process.env.PATH_URL_BACKEND+`/api/posts/${params.id}`, {
next: { revalidate: 10 },
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
const data = await res.json();
return NextResponse.json(data)
}
Các bạn có thể nhìn vào đoạn code trên mình có sử dụng next: { revalidate: 10 }, nó dùng để lưu bộ nhớ dữ liệu trong vòng 10 giây, tùy vào ứng dụng của bạn mà cấu hình nhé
+ app/post/page.tsx : Hiển thị danh sách bài viết ra cho người dùng xem
"use client";
import React,{useEffect, useState} from "react";
import useSWR from "swr";
import { fetcher } from "../libs";
import Post from "../components/Post";
import { PostModel } from "../types";
import Link from "next/link";
export default function Posts() {
const [posts,setPosts] = useState<PostModel[]>([]);
const { data, error, isLoading } = useSWR<any>(`/api/posts`, fetcher);
useEffect(()=>{
if(data && data.result.data)
{
console.log(data.result.data);
setPosts(data.result.data);
}
},[data,isLoading]);
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return null;
let delete_Post : PostModel['deletePost']= async (id:number) => {
const res = await fetch(`/api/posts/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
});
const content = await res.json();
if(content.success>0)
{
setPosts(posts?.filter((post:PostModel)=>{ return post.id !== id }));
}
}
return (
<div className="w-full max-w-7xl m-auto">
<table className="w-full border-collapse border border-slate-400">
<caption className="caption-top py-5 font-bold text-green-500 text-2xl">
List Posts - Counter :
<span className="text-red-500 font-bold">{ posts?.length}</span>
</caption>
<thead>
<tr className="text-center">
<th className="border border-slate-300">ID</th>
<th className="border border-slate-300">Title</th>
<th className="border border-slate-300">Hide</th>
<th className="border border-slate-300">Created at</th>
<th className="border border-slate-300">Modify</th>
</tr>
</thead>
<tbody>
<tr>
<td colSpan={5}>
<Link href={`/post/create`} className="bg-green-500 p-2 inline-block text-white">Create</Link>
</td>
</tr>
{
posts && posts.map((item : PostModel)=><Post key={item.id} {...item} deletePost = {delete_Post} />)
}
</tbody>
</table>
</div>
);
}
Có nhiều thứ bên trong đoạn code trên mình đã chia sẻ với mọi người bài viết trước như : SWR Bạn nào chưa xem thì xem lại tại đây nhé : Create A Example Handling Data Fetching With SWR In NextJS
Các bạn xem đoạn code này, mình có tạo một function để bắt sự kiện xóa một bài viết
let delete_Post : PostModel['deletePost']= async (id:number) => {
const res = await fetch(`/api/posts/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
});
const content = await res.json();
if(content.success>0)
{
setPosts(posts?.filter((post:PostModel)=>{ return post.id !== id }));
}
}
----------
//chèn function đó qua component để bắt sự kiện click delete
posts && posts.map((item : PostModel)=><Post key={item.id} {...item} deletePost = {delete_Post} />
+ app/components/Post.ts : component hiện bài viết và xử lý sự kiện click xóa bài viết
import React from 'react'
import { PostModel } from '../types'
import Link from 'next/link'
export default function Post(params: PostModel) {
return (
<tr>
<td className='w-10 border border-slate-300 text-center'>{params.id}</td>
<td className='border border-slate-300'>{params.title}</td>
<td className='border border-slate-300 text-center'>{params.publish>0?'open':'hide'}</td>
<td className='border border-slate-300 text-center'>{params.created_at}</td>
<td className='w-52 border border-slate-300'>
<span onClick={()=>params.deletePost(params.id)} className='bg-red-500 p-2 inline-block text-white text-sm'>Delete</span>
<Link href={`/post/edit/${params.id}`} className='bg-yellow-500 p-2 inline-block ml-3 text-white text-sm'>Edit</Link>
<Link href={`/post/read/${params.id}`} className='bg-yellow-500 p-2 inline-block ml-3 text-white text-sm'>View</Link>
</td>
</tr>
)
}
Bắt sự kiện click xóa bài viết : params.deletePost(params.id)
+ app/post/create/page.tsx : Tạo form để nhập thông tin thêm bài viết , đoạn code dưới mình có dùng useState để lưu dữ liệu, nói chung nó giống như bên React. Nên mình sẽ bỏ qua phần giải thích này
"use client"
import React, {useState } from 'react'
import { useRouter } from 'next/navigation'
export default function PostCreate() {
const router = useRouter()
const [title, setTitle] =useState<string>('');
const [body, setBody] = useState<string>('');
const addPost = async (e: any) => {
e.preventDefault()
if (title!="" && body!="") {
const formData = {
title: title,
content: body
}
const add = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const content = await add.json();
if(content.success>0)
{
router.push('/post');
}
}
};
return (
<form className='w-full' onSubmit={addPost}>
<span className='font-bold text-yellow-500 py-2 block underline text-2xl'>Form Add</span>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Title</label>
<input type='text' name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' onChange={(e:any)=>setTitle(e.target.value)}/>
</div>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Content</label>
<textarea name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' onChange={(e:any)=>setBody(e.target.value)} />
</div>
<div className='w-full py-2'>
<button className="w-20 p-2 text-white border-gray-200 border-[1px] rounded-sm bg-green-400">Submit</button>
</div>
</form>
)
}
+ app/post/edit/[id]/page.tsx : Edit bài viết, bằng cách ta lấy ID của bài viết , request đến /api/posts/edit/[id]/route.ts để lấy dữ liệu cần chỉnh sửa
"use client"
import React, {useState,useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { fetcher } from '@/app/libs'
import useSWR from 'swr'
export default function PostEdit({params} :{params:{id:number}}) {
const router = useRouter()
const {data : post,isLoading, error} = useSWR(`/api/posts/${params.id}`,fetcher)
const [title, setTitle] =useState<string>('');
const [body, setBody] = useState<string>('');
useEffect(()=>{
if(post){
setTitle(post.result.title)
setBody(post.result.content)
}
},[post, isLoading])
const updatePost = async (e: any) => {
e.preventDefault()
if (title!="" && body!="") {
const formData = {
title: title,
content: body
}
const res = await fetch(`/api/posts/${params.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const content = await res.json();
if(content.success>0)
{
router.push('/post');
}
}
};
if(isLoading) return <div><span>Loading...</span></div>
if (!post) return null;
return (
<form className='w-full' onSubmit={updatePost}>
<span className='font-bold text-yellow-500 py-2 block underline text-2xl'>Form Add</span>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Title</label>
<input type='text' name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' value={title} onChange={(e:any)=>setTitle(e.target.value)}/>
</div>
<div className='w-full py-2'>
<label htmlFor="" className='text-sm font-bold py-2 block'>Content</label>
<textarea name='title' className='w-full border-[1px] border-gray-200 p-2 rounded-sm' value={body} onChange={(e:any)=>setBody(e.target.value)} />
</div>
<div className='w-full py-2'>
<button className="w-20 p-2 text-white border-gray-200 border-[1px] rounded-sm bg-green-400">Submit</button>
</div>
</form>
)
}
+ app/post/read/[id]/page.tsx :Tương tự như của Edit, nhưng ở route này ta chỉ cần hiện thị thông tin cho người dùng xem
'use client'
import { fetcher } from '@/app/libs'
import useSWR from 'swr'
export default function Detail({params}: {params:{id :number}}) {
const {data: post, isLoading, error} = useSWR(`/api/posts/${params.id}`,fetcher)
if(isLoading) return <div><span>Loading...</span></div>
if (!post) return null;
return (
<div className='w-full'>
<h2 className='text-center font-bold text-3xl py-3'>{post.result.title}</h2>
<div className='w-full max-w-4xl m-auto border-[1px] p-3 border-gray-500 rounded-md'>
<p dangerouslySetInnerHTML={{ __html: post.result.content}}></p>
</div>
</div>
)
}
+ app/page.tsx : import component /app/post/page.tsx , để hiện thị màn hình chính của trang chủ
import Posts from './post/page'
export default function Home() {
return (
<Posts />
)
}
+ app/layout.tsx : bố cục ứng dụng
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import Header from './components/Header'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<Header />
<div className='w-full max-w-7xl mt-4 m-auto'>
{children}
</div>
</body>
</html>
)
}