🐝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