🐝MERN CRUD applications Todo App Back End , Front End (ok)
https://github.com/MechanicJosh/Todo-Crud-App
https://github.com/covalence-io/ts-react-express-esbuild
Back End
Part 1. Build Scripts buil and start
package.json
{
"name": "todo",
"version": "1.0.0",
"main": "dist/server.js",
"scripts": {
"build:server": "node esbuild-config/server.prod.mjs",
"build": "npm-run-all --sequential build:*",
"start": "node dist/server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"bootstrap": "^5.3.3",
"cors": "^2.8.5",
"express": "^4.21.1",
"mysql2": "^3.11.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.0.2"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/node": "^22.10.1",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.1",
"@types/typescript": "^2.0.0",
"esbuild": "^0.24.0",
"nodemon": "^3.1.7",
"npm-run-all": "^4.1.5",
"typescript": "^5.7.2"
}
}
tsconfig.json
{
"compilerOptions": {
"sourceMap": true,
"target": "es2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"rootDir": "./src",
"moduleResolution": "node",
"resolveJsonModule": true
},
"exclude": ["node_modules"]
}
src\server\tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist",
"rootDir": ".",
"types": ["node"]
},
"include": ["./**/*"]
}
esbuild-config\server.prod.mjs
import * as esbuild from 'esbuild';
try {
await esbuild.build({
entryPoints: ['src/server/server.ts'],
bundle: true,
sourcemap: false,
minify: true,
platform: 'node',
target: ['node18.6'],
packages: 'external',
define: {
'process.env.NODE_ENV': "'production'"
},
outfile: 'dist/server.js'
});
console.log('Server bundled successfully for production!');
} catch (error) {
console.error('An error occurred during bundling:', error);
process.exit(1);
}
src\server\server.ts
console.log("aaa1");
Part 2. Test Connect Database
todo_app.sql
CREATE TABLE `todos` (
`id` int(50) NOT NULL,
`description` varchar(255) NOT NULL,
`isCompleted` tinyint(4) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `todos` (`id`, `description`, `isCompleted`) VALUES
(1, 'Todo 1', 0),
(2, 'Todo 2', 0),
(3, 'Todo 3', 0),
(4, 'Todo 4', 0);
ALTER TABLE `todos`
ADD PRIMARY KEY (`id`);
ALTER TABLE `todos`
MODIFY `id` int(50) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
COMMIT;
src\server\db\connection.ts
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: '',
database: 'todo_db'
});
export default pool;
src\server\db\queryUtils.ts
import pool from './connection';
export async function SelectQuery<T>(queryString: string, params?: any) {
const [results] = await pool.execute(queryString, params);
return results as T[];
}
export async function ModifyQuery(queryString: string, params?: any) {
const [results] = await pool.query(queryString, params);
return results;
}
src\server\db\tables\todos.ts
import { SelectQuery, ModifyQuery } from '../queryUtils';
export interface ITodosRow {
id: number;
description: string;
isCompleted: 0 | 1;
}
export function getAll() {
return SelectQuery<ITodosRow>('SELECT * FROM todos;');
}
export function getOne(id: number) {
return SelectQuery<ITodosRow>('SELECT * FROM todos WHERE id = ?;', [id]);
}
export function insert(todoItem: string) {
return ModifyQuery('INSERT INTO todos (description) VALUE (?);', [todoItem])
}
src\server\db\index.ts
import * as todos from './tables/todos';
export default {
todos
}
Part 3. Test apiRouter
Part 3.1 GET /api/todos
src\server\server.ts
import express from 'express';
import cors from 'cors';
import apiRouter from './routes';
import db from './db';
const app = express();
// Test Connect Database
db.todos.getAll().then(todos => console.log(todos));
// Test apiRouter
app.use('/api', apiRouter);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
src\server\routes\index.ts
import { Router } from 'express';
import todosRouter from './todos';
//api router
const router = Router();
router.use('/todos', todosRouter);
export default router;
src\server\routes\todos.ts
import { Router } from 'express';
import db from '../db';
const router = Router();
// GET /api/todos
router.get('/', async (req, res) => {
try {
const todos = await db.todos.getAll();
res.json(todos);
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
export default router;
Part 3.2 GET /api/todos/1
src\server\routes\todos.ts
import { Router } from 'express';
import db from '../db';
const router = Router();
// GET /api/todos
router.get('/', async (req, res) => {
try {
const todos = await db.todos.getAll();
res.json(todos);
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
//GET /api/todos/123
router.get('/:id', async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const [todo] = await db.todos.getOne(id);
res.json(todo);
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
export default router;
😌 Chú ý: Phải bổ sung code này app.use(express.json());
src\server\server.ts
Không thì báo lỗi giống như này
Part 3.3 POST/api/todos
src\server\routes\todos.ts
import { Router } from 'express';
import db from '../db';
const router = Router();
// GET /api/todos
router.get('/', async (req, res) => {
try {
const todos = await db.todos.getAll();
res.json(todos);
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
//GET /api/todos/123
router.get('/:id', async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const [todo] = await db.todos.getOne(id);
res.json(todo);
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
//POST /api/todos
router.post('/', async (req, res) => {
try {
const newTodo = req.body;
const result = await db.todos.insert(newTodo.description);
res.json({ message: 'Todo created', id: result })
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
export default router;
Part 3.4 POST/api/todos
src\server\db\tables\todos.ts
import { SelectQuery, ModifyQuery } from '../queryUtils';
export interface ITodosRow {
id: number;
description: string;
isCompleted: 0 | 1;
}
export function getAll() {
return SelectQuery<ITodosRow>('SELECT * FROM todos;');
}
export function getOne(id: number) {
return SelectQuery<ITodosRow>('SELECT * FROM todos WHERE id = ?;', [id]);
}
export function insert(todoItem: string) {
return ModifyQuery('INSERT INTO todos (description) VALUE (?);', [todoItem])
}
export function update(id:number,description: string, isCompleted:number) {
return ModifyQuery('UPDATE todos SET description=?,isCompleted=? WHERE id=?;', [description,isCompleted,id]);
}
src\server\routes\todos.ts
import { Router } from 'express';
import db from '../db';
const router = Router();
// GET /api/todos
router.get('/', async (req, res) => {
try {
const todos = await db.todos.getAll();
res.json(todos);
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
//GET /api/todos/123
router.get('/:id', async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const [todo] = await db.todos.getOne(id);
res.json(todo);
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
//POST /api/todos
router.post('/', async (req, res) => {
try {
const newTodo = req.body;
const result = await db.todos.insert(newTodo.description);
res.json({ message: 'Todo created', id: result })
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
//PUT /api/todos
router.put('/', async (req, res) => {
try {
console.log(req.body);
const newTodo = req.body;
const result = await db.todos.update(newTodo.id,newTodo.description,newTodo.isCompleted);
res.json({ message: 'Update created', id: result })
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
export default router;
Front End
Chú ý: Đây là cách để chạy riêng dự án front-end trong bài này chúng ta đã có code để chạy chung một lúc. Do chúng ta đang làm từng phần nên sẽ tách ra để dễ hiểu.
src\client\tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "react",
"outDir": "../../public/static",
"rootDir": "."
},
"include": [
"./**/*"
]
}
esbuild-config\client.dev.mjs
import * as esbuild from 'esbuild';
import * as sass from 'sass';
import { sassPlugin } from 'esbuild-sass-plugin';
let ctx;
try {
ctx = await esbuild.context({
entryPoints: ['src/client/index.tsx'],
bundle: true,
minify: false,
sourcemap: true,
outfile: 'public/static/bundle.js',
plugins: [sassPlugin({ type: 'style', logger: sass.Logger.silent, quietDeps: true })],
define: {
'process.env.NODE_ENV': "'development'"
}
});
await ctx.watch();
const { host, port } = await ctx.serve({
servedir: 'public',
fallback: 'public/index.html',
port: 9000
});
var hostct = "localhost";
console.info(`Hot refresh at http://${hostct}:${port}`);
} catch (error) {
console.error('An error occurred:', error);
process.exit(1);
}
package.json
{
"name": "todo",
"version": "1.0.0",
"main": "front.js",
"scripts": {
"watch:client": "node esbuild-config/client.dev.mjs",
"watch:restart-server": "nodemon --config nodemon.json",
"front": "npm-run-all --parallel watch:*"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@types/bootstrap": "^5.2.10",
"bootstrap": "^5.3.3",
"cors": "^2.8.5",
"express": "^4.21.1",
"mysql2": "^3.11.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.0.2"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/node": "^22.10.1",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.1",
"@types/typescript": "^2.0.0",
"esbuild": "^0.24.0",
"esbuild-sass-plugin": "^3.3.1",
"nodemon": "^3.1.7",
"npm-run-all": "^4.1.5",
"sass": "^1.82.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
}
}
public\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="theme-color" content="#ff6ad5" />
<title>Todos App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/static/bundle.js"></script>
</body>
</html>
src\client\App.tsx
import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import Home from './views/Home';
interface AppProps {}
const App = (props: AppProps) => {
return (
<BrowserRouter>
<div className='px-5 py-2'>
<Link to="/">Home</Link>
</div>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</BrowserRouter>
);
}
export default App;
src\client\index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles/app.scss';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
src\client\styles\app.scss
// Vaporwave Color Scheme
// Colors
$body-bg: #0091ea; // Bright Blue
$primary: #ff6ad5; // Neon Pink
$secondary: #ffdb58; // Bright Yellow
$success: #00f764; // Neon Green
$info: #01cdfe; // Bright Cyan
$warning: #ff61a6; // Pinkish
$danger: #ff2965; // Neon Red
$light: #9A83D4; // Pastel Purple
$dark: #260e37; // Dark Blueish Purple
// Link Colors
$link-color: #01cdfe; // Bright Cyan
$link-hover-color: darken($link-color, 10%);
// Bootstrap Import
@import '~bootstrap/scss/bootstrap.scss';
src\client\views\Home.tsx
import React from 'react';
interface HomeProps {}
const Home = (props: HomeProps) => {
return (
<main className="container mt-5" >
<section className="row justify-content-center" >
<h1 className="text-center text-primary">Welcome to my Todos Full Stack CRUD App!</h1>
</section>
</main>
);
}
export default Home;
Part 4 Create Home Page
package.json
{
"name": "todo",
"version": "1.0.0",
"main": "front.js",
"scripts": {
"watch:client": "node esbuild-config/client.dev.mjs",
"watch:restart-server": "nodemon --config nodemon.json",
"front": "npm-run-all --parallel watch:*"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@types/bootstrap": "^5.2.10",
"bootstrap": "^5.3.3",
"cors": "^2.8.5",
"express": "^4.21.1",
"mysql2": "^3.11.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.0.2"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/node": "^22.10.1",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.1",
"@types/typescript": "^2.0.0",
"esbuild": "^0.24.0",
"esbuild-sass-plugin": "^3.3.1",
"nodemon": "^3.1.7",
"npm-run-all": "^4.1.5",
"sass": "^1.82.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
}
}
nodemon.json
{
"watch": ["src/server", ".env"],
"ignore": ["src/**/*.test.ts", "src/client/", "public/"],
"ext": "ts,mjs,js,json",
"legacyWatch": true
}
Part 5.1 Create Todos Page
src\client\views\Todos.tsx
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
interface TodosProps { }
const Todos = (props: TodosProps) => {
return (
<main className="container mt-5" >
Test Todos
</main>
);
}
export default Todos;
src\client\App.tsx
import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import Home from './views/Home';
import Todos from './views/Todos';
interface AppProps {}
const App = (props: AppProps) => {
return (
<BrowserRouter>
<div className='px-5 py-2'>
<Link to="/" className='me-1'>Home</Link>
<Link to="/todos" className='me-1'>Todos</Link>
</div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Part 5.2 Build fetchData
src\client\services\fetchData.ts
const BASE_URL = process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '';
export async function fetchData(endpoint: string, method: string = 'GET', payload: any = null) {
try {
const options: RequestInit = {
method: method,
headers: {}
}
if (payload && method !== 'GET') {
options.headers = {
'Content-Type': 'application/json'
},
options.body = JSON.stringify(payload);
}
const response = await fetch(`${BASE_URL}${endpoint}`, options);
if (!response.ok) {
throw new Error(`HTTP Error Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`Fetch error: ${error}`);
throw error;
}
}
Part 6 Create Details
src\client\App.tsx
import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import Home from './views/Home';
import Todos from './views/Todos';
import Details from './views/Details';
interface AppProps {}
const App = (props: AppProps) => {
return (
<BrowserRouter>
<div className='px-5 py-2'>
<Link to="/" className='me-1'>Home</Link>
<Link to="/todos" className='me-1'>Todos</Link>
</div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
<Route path="/todos/:id" element={<Details />} />
</Routes>
</BrowserRouter>
);
}
export default App;
src\client\views\Details.tsx
import React, { useState, useEffect } from 'react';
import { Link, useParams } from 'react-router-dom';
import type { ITodo } from '../types';
import { fetchData } from '../services/fetchData';
interface DetailsProps { }
const Details = (props: DetailsProps) => {
const { id } = useParams();
const [data, setData] = useState<ITodo | null>(null);
useEffect(() => {
fetchData(`/api/todos/${id}`).then(data => setData(data));
}, []);
return (
<main className="container mt-5" >
<section className="row justify-content-center" >
<div className="col-12 col-md-6">
<div className="card shadow">
<div className="card-body">
<h2 className="card-title">Todo Item # {id}</h2>
<p className="card-text">{data?.description}</p>
<Link to="/todos" className='btn btn-outline-info'>Go Back</Link>
</div>
</div>
</div>
</section>
</main>
);
}
export default Details;
Part 7 Create Add page
src\server\routes\todos.ts
import { Router } from 'express';
import db from '../db';
const router = Router();
// GET /api/todos
router.get('/', async (req, res) => {
try {
const todos = await db.todos.getAll();
res.json(todos);
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
//GET /api/todos/123
router.get('/:id', async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const [todo] = await db.todos.getOne(id);
res.json(todo);
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
//POST /api/todos
router.post('/', async (req, res) => {
try {
const newTodo = req.body;
const result = await db.todos.insert(newTodo.description);
res.json({ message: 'Todo created', id: result.insertId })
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
//PUT /api/todos
router.put('/', async (req, res) => {
try {
const newTodo = req.body;
const result = await db.todos.update(newTodo.id,newTodo.description,newTodo.isCompleted);
res.json({ message: 'Update created', id: result })
} catch (error) {
console.log(error)
res.status(500).json({ message: 'Internal Server Error', error });
}
});
export default router;
src\server\db\queryUtils.ts
import pool from './connection';
import type { ResultSetHeader } from 'mysql2';
export async function SelectQuery<T>(queryString: string, params?: any): Promise<Partial<T>[]> {
const [results] = await pool.execute(queryString, params);
return results as T[];
}
export async function ModifyQuery(queryString: string, params?: any) {
const [results] = await pool.query(queryString, params);
return results;
}
src\client\App.tsx
import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import Home from './views/Home';
import Todos from './views/Todos';
import Details from './views/Details';
import Add from './views/Add';
interface AppProps {}
const App = (props: AppProps) => {
return (
<BrowserRouter>
<div className='px-5 py-2'>
<Link to="/" className='me-1'>Home</Link>
<Link to="/todos" className='me-1'>Todos</Link>
<Link to="/todos/new">Add</Link>
</div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
<Route path="/todos/:id" element={<Details />} />
<Route path="/todos/new" element={<Add />} />
</Routes>
</BrowserRouter>
);
}
export default App;
src\client\views\Add.tsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { fetchData } from '../services/fetchData';
interface AddProps { }
const Add = (props: AddProps) => {
const navigate = useNavigate();
const [value, setValue] = useState<string>('');
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
fetchData('/api/todos', 'POST', { description: value }).then(data =>
{
navigate(`/todos/${data.id}`);
}
);
};
return (
<main className="container mt-5" >
<section className="row justify-content-center" >
<div className="col-12 col-md-6">
<form className="p-4 shadow border">
<label htmlFor="description">Todo Item Description</label>
<input value={value} onChange={e => setValue(e.target.value)} type="text" className="form-control" placeholder="Do All The Things" />
<button onClick={handleSubmit} className="mt-3 btn btn-primary">Add Todo</button>
</form>
</div>
</section>
</main>
);
}
export default Add;
Part 8 Edit
src\client\App.tsx
import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import Home from './views/Home';
import Todos from './views/Todos';
import Details from './views/Details';
import Add from './views/Add';
import Edit from './views/Edit';
interface AppProps {}
const App = (props: AppProps) => {
return (
<BrowserRouter>
<div className='px-5 py-2'>
<Link to="/" className='me-1'>Home</Link>
<Link to="/todos" className='me-1'>Todos</Link>
<Link to="/todos/new">Add</Link>
</div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
<Route path="/todos/:id" element={<Details />} />
<Route path="/todos/new" element={<Add />} />
<Route path="/todos/edit/:id" element={<Edit />} />
</Routes>
</BrowserRouter>
);
}
export default App;
src\client\views\Edit.tsx
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { fetchData } from '../services/fetchData';
import { ITodo } from '../types';
interface AddProps { }
const Edit = (props: AddProps) => {
const navigate = useNavigate();
const { id } = useParams();
const [InputDes, setDataName] = useState("");
const [InputIs, setDataIs] = useState(1);
const handleNameChange = (name:string) => {
setDataName(name);
};
const handleNumberChange = (isCom:any) => {
setDataIs(isCom);
};
useEffect(() => {
fetchData(`/api/todos/${id}`).then(data => {
setDataName(data.description);
setDataIs(data.isCompleted);
});
}, []);
const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
fetchData('/api/todos', 'PUT', {id: id, description:InputDes,isCompleted:InputIs}).then(dat =>
{
navigate(`/todos/${id}`);
}
);
};
return (
<main className="container mt-5" >
<section className="row justify-content-center" >
<div className="col-12 col-md-6">
<form className="p-4 shadow border">
<label htmlFor="description">Todo Item Description</label>
<input id="description" name="description" value={InputDes} type="text" onChange={e => handleNameChange(e.target.value)} className="form-control" placeholder="Do All The Things" />
<label htmlFor="isCompleted">Todo Item isCompleted</label>
<input id="isCompleted" name="iscompleted" value={InputIs} type="number" onChange={e => handleNumberChange(e.target.value)} className="form-control" placeholder="Do All The Things" />
<button onClick={handleSubmit} className="mt-3 btn btn-primary">Edit Todo</button>
</form>
</div>
</section>
</main>
);
}
export default Edit;
Kết hợp dự án Front-end và Back-end cùng một lúc 👍
🤡🤡🤡 Build yarn dev Full
esbuild-config\client.dev.mjs
import * as esbuild from 'esbuild';
import * as sass from 'sass';
import { sassPlugin } from 'esbuild-sass-plugin';
let ctx;
try {
ctx = await esbuild.context({
entryPoints: ['src/client/index.tsx'],
bundle: true,
minify: false,
sourcemap: true,
outfile: 'public/static/bundle.js',
plugins: [sassPlugin({ type: 'style', logger: sass.Logger.silent, quietDeps: true })],
define: {
'process.env.NODE_ENV': "'development'"
}
});
await ctx.watch();
console.log('Watching client...');
const { host, port } = await ctx.serve({
servedir: 'public',
fallback: 'public/index.html',
port: 6001
});
var hostct = "localhost";
console.info(`Hot refresh at http://${hostct}:${port}`);
} catch (error) {
console.error('An error occurred:', error);
process.exit(1);
}
esbuild-config\client.prod.mjs
import * as esbuild from "esbuild";
import * as sass from "sass";
import { sassPlugin } from "esbuild-sass-plugin";
try {
await esbuild.build({
entryPoints: ["src/client/index.tsx"],
bundle: true,
sourcemap: false,
minify: true,
outfile: "public/static/bundle.js",
define: {
"process.env.NODE_ENV": "'production'",
},
plugins: [
sassPlugin({
type: "style",
quietDeps: true,
logger: sass.Logger.silent,
}),
],
});
console.log("Client bundled successfully for production!");
} catch (error) {
console.error("An error occurred during bundling:", error);
process.exit(1);
}
esbuild-config\server.dev.mjs
import * as esbuild from 'esbuild';
let ctx;
try {
ctx = await esbuild.context({
entryPoints: ['src/server/server.ts'],
bundle: true,
sourcemap: true,
minify: false,
platform: 'node',
target: ['node18.6'],
packages: 'external',
define: {
'process.env.NODE_ENV': "'development'"
},
outfile: 'dist/server.js'
});
await ctx.watch();
console.log('Watching server...');
} catch (error) {
console.error('An error occurred:', error);
process.exit(1);
}
esbuild-config\server.prod.mjs
import * as esbuild from "esbuild";
try {
await esbuild.build({
entryPoints: ["src/server/server.ts"],
bundle: true,
sourcemap: false,
minify: true,
platform: "node",
target: ["node18.6"],
packages: "external",
define: {
"process.env.NODE_ENV": "'production'",
},
outfile: "dist/server.js",
});
console.log("Server bundled successfully for production!");
} catch (error) {
console.error("An error occurred during bundling:", error);
process.exit(1);
}
package.json
{
"name": "todo",
"version": "1.0.0",
"main": "dist/server.js",
"scripts": {
"watch:client": "node esbuild-config/client.dev.mjs",
"watch:server": "node esbuild-config/server.dev.mjs",
"watch:restart-server": "nodemon --config nodemon.json",
"dev": "npm-run-all --parallel watch:*",
"build:client": "node esbuild-config/client.prod.mjs",
"build:server": "node esbuild-config/server.prod.mjs",
"build": "npm-run-all --sequential build:*",
"start": "node dist/server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"bootstrap": "^5.3.3",
"cors": "^2.8.5",
"express": "^4.21.1",
"mysql2": "^3.11.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.0.2"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/node": "^22.10.1",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.1",
"@types/typescript": "^2.0.0",
"esbuild": "^0.24.0",
"esbuild-sass-plugin": "^3.3.1",
"nodemon": "^3.1.7",
"npm-run-all": "^4.1.5",
"sass": "^1.82.0",
"typescript": "^5.7.2"
}
}
😌 Chú ý: Hiểu rõ lý do tại sao lại dùng cors
Nếu không dùng đoạn code này nó sẽ không cho bên khác lấy dữ liệu qua api
Sau khi thêm nó lấy được dữ liêu như này
src\server\server.ts
import express from 'express';
import cors from 'cors';
import apiRouter from './routes';
import db from './db';
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = process.env.NODE_ENV === 'development';
const app = express();
if (isDevelopment) {
app.use(cors());
}
// Test Connect Database
db.todos.getAll().then(todos => console.log(todos));
// Test apiRouter
app.use(express.json());
app.use('/api', apiRouter);
// Test env
if (isProduction) {
app.use(express.static('public'));
}
if (isProduction) {
app.get('*', (req, res) => {
res.sendFile('index.html', { root: 'public' });
});
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Cập nhật lại
src\client\views\Todos.tsx
<main className="container mt-5" >
<section className="row justify-content-center" >
<div className="col-12 col-md-6">
<ul className="list-group">
{list.map(todo => (
<li key={`todo-item-${todo.id}`} className="list-group-item d-flex justify-content-between align-items-center">
<span>{todo.description}</span>
<Link to={`/todos/${todo.id}`} className="btn btn-small btn-secondary">Details</Link>
</li>
))}
</ul>
</div>
</section>
</main>
Result 👇
Last updated