React、TypeScript、NodeJS 和 MongoDB 搭建 Todo App

在本教程中,我們將在服務(wù)器和客戶端使用 TypeScript、React、NodeJS、Express 和 MongoDB 從頭開始構(gòu)建一個(gè) Todo 應(yīng)用程序。
我們從設(shè)計(jì) API 開始。
用 NodeJS, Express, MongoDB 和 TypeScript 設(shè)計(jì) API 啟動(dòng) 創(chuàng)建 Todo 類型 創(chuàng)建 Todo 模塊 創(chuàng)建 API 控制器 獲取、新增、更新和刪除 Todo 創(chuàng)建 API 路由 創(chuàng)建服務(wù)器 用 React 和 TypeScript 創(chuàng)建客戶端 啟動(dòng) 創(chuàng)建 Todo 類型 從 API 獲取數(shù)據(jù) 創(chuàng)建組件 添加 Todo 表單 展示 Todo 獲取和展示數(shù)據(jù) 資源
用 NodeJS, Express, MongoDB 和 TypeScript 設(shè)計(jì) API
啟動(dòng)
如果你是新手,可以看看《TypeScript 實(shí)用指南》,或者從《如何用 Node JS、Express 和 MongoDB 從頭創(chuàng)建 API》。如果你有一定經(jīng)驗(yàn)了,可以直接開始。
在終端上運(yùn)行這個(gè)命令,創(chuàng)建一個(gè)新的 NodeJS 應(yīng)用程序:
?yarn?init
它會(huì)詢問幾個(gè)問題,然后初始化應(yīng)用程序。你可以通過向命令中添加 ?-y??標(biāo)志來(lái)跳過。
然后,按照以下目錄構(gòu)建項(xiàng)目:
├──?dist
├──?node_modules
├──?src
???├──?app.ts
???├──?controllers
???|??└──?todos
???|?????└──?index.ts
???├──?models
???|??└──?todo.ts
???├──?routes
???|??└──?index.ts
???└──?types
??????└──?todo.ts
├──?nodemon.json
├──?package.json
├──?tsconfig.json
如你所見,這個(gè)文件結(jié)構(gòu)相對(duì)簡(jiǎn)單。代碼編譯成純 JavaScript 后,dist 目錄將用作輸出文件夾。
我們還有一個(gè) ?app.ts,它是服務(wù)器的入口??刂破鳌㈩愋秃吐酚梢苍谒鼈兏髯砸运鼈兠牡奈募A中。
現(xiàn)在,我們需要配置 ?tsconfig.json,使編譯器運(yùn)行我們的首選項(xiàng)。
tsconfig.json
{
??"compilerOptions":?{
????"target":?"es6",
????"module":?"commonjs",
????"outDir":?"dist/js",
????"rootDir":?"src",
????"strict":?true,
????"esModuleInterop":?true,
????"forceConsistentCasingInFileNames":?true
??},
??"include":?["src/**/*"],
??"exclude":?["src/types/*.ts",?"node_modules",?".vscode"]
}
這里強(qiáng)調(diào)四個(gè)主要屬性:
outDir: 告訴編譯器把編譯好的代碼放進(jìn) ?dist/js??文件夾
rootDir: 告訴 TypeScript 編譯 src 文件夾中的每個(gè) .ts 文件
include: 告訴編譯器包含 src 目錄和子目錄中的文件
exclude: 在編譯時(shí)會(huì)排除數(shù)組中的文件或文件夾
現(xiàn)在我們安裝依賴項(xiàng),使項(xiàng)目可以使用 TypeScript。因?yàn)槟J(rèn)情況下,這個(gè)應(yīng)用程序會(huì)使用 JavaScript。
在 NodeJS 應(yīng)用程序中有兩種使用 TypeScript 的方法,要么在項(xiàng)目中本地安裝使用,要么在電腦中全局安裝使用?;趥€(gè)人喜好,我會(huì)選擇后者。但如果你想,你也可以堅(jiān)持使用本地安裝使用的方式。
現(xiàn)在,讓我們?cè)诮K端上執(zhí)行以下命令來(lái)安裝 TypeScript。
?yarn?add?typescript?-g
這個(gè) ?g??標(biāo)志允許全局安裝 TypeScript,這樣它就能在計(jì)算機(jī)任何地方使用。
接下來(lái),為了使用 Express 和 MongoDB,我們安裝一些依賴項(xiàng)。
yarn?add?express?cors?mongoose
我們還需要安裝它們的類型作為開發(fā)依賴項(xiàng),幫助 TypeScript 編譯器理解這些包。
yarn?add?-D?@types/node?@types/express?@types/mongoose?@types/cors
現(xiàn)在,TypeScript 不會(huì)再對(duì)你提示錯(cuò)誤——它將使用這些類型來(lái)定義我們剛剛安裝的庫(kù)。
我們還需要安裝其他依賴項(xiàng),以便能夠編譯 TypeScript 代碼并同時(shí)啟動(dòng)服務(wù)器。
yarn?add?-D?concurrently?nodemon
有了這些,我們現(xiàn)在就可以更新 ?package.json??的 scripts 來(lái)啟動(dòng)服務(wù)器。
package.json
"scripts":?{
????"build":?"tsc",
????"start":?"concurrently?\"tsc?-w\"?\"nodemon?dist/js/app.js\""
??}
concurrently??幫助編譯 TypeScript 代碼,持續(xù)觀察變化,同時(shí)啟動(dòng)服務(wù)器。也就是說(shuō),我們現(xiàn)在可以啟動(dòng)服務(wù)器了——但是,我們還沒有創(chuàng)建一些有意義的東西。所以,讓我們?cè)谙乱还?jié)中解決這個(gè)問題。創(chuàng)建 Todo 類型
types/todo.ts
import?{?Document?}?from?"mongoose"
export?interface?ITodo?extends?Document?{
??name:?string
??description:?string
??status:?boolean
}
這里,我們有了繼承 ?mongoose??提供的 ?Document??類型的 Todo 接口。稍后我們將使用它與 MongoDB 交互。也就是說(shuō),我們現(xiàn)在可以定義 Todo 模塊。
創(chuàng)建 Todo 模塊
models/todo.ts
import?{?ITodo?}?from?"./../types/todo"
import?{?model,?Schema?}?from?"mongoose"
const?todoSchema:?Schema?=?new?Schema(
??{
????name:?{
??????type:?String,
??????required:?true,
????},
????description:?{
??????type:?String,
??????required:?true,
????},
????status:?{
??????type:?Boolean,
??????required:?true,
????},
??},
??{?timestamps:?true?}
)
export?default?model("Todo",?todoSchema)
首先導(dǎo)入 ?ITodo??接口和 一些 ?mongoose??導(dǎo)出的模塊,后者是幫助定義 Todo schema 和在導(dǎo)出前把 ITodo 作為類型參數(shù)傳入 ?model??。
這樣,我們現(xiàn)在就可以在其他文件中使用 Todo 模塊來(lái)與數(shù)據(jù)庫(kù)交互。
創(chuàng)建 API 控制器
獲取、新增、更新和刪除 Todos
controllers/todos/index.ts
import?{?Response,?Request?}?from?"express"
import?{?ITodo?}?from?"./../../types/todo"
import?Todo?from?"../../models/todo"
const?getTodos?=?async?(req:?Request,?res:?Response):?Promise<void>?=>?{
??try?{
????const?todos:?ITodo[]?=?await?Todo.find()
????res.status(200).json({?todos?})
??}?catch?(error)?{
????throw?error
??}
}
這里,我們首先需要從 ?express??導(dǎo)入一些類型,因?yàn)槲蚁腼@式指明類型。如果你想,你可以讓 TypeScript 幫你推斷。
接下來(lái),我們使用 getTodos() 函數(shù)來(lái)獲取數(shù)據(jù),它接收 ?req??和 ?res??參數(shù)并返回 promise。
在前面創(chuàng)建的 Todo 模塊的幫助下,我們現(xiàn)在可以從 MongoDB 獲取數(shù)據(jù)并返回 Todo 數(shù)組。
controllers/todos/index.ts
const?addTodo?=?async?(req:?Request,?res:?Response):?Promise<void>?=>?{
??try?{
????const?body?=?req.body?as?Pick"name"?|?"description"?|?"status">
????const?todo:?ITodo?=?new?Todo({
??????name:?body.name,
??????description:?body.description,
??????status:?body.status,
????})
????const?newTodo:?ITodo?=?await?todo.save()
????const?allTodos:?ITodo[]?=?await?Todo.find()
????res
??????.status(201)
??????.json({?message:?"Todo?added",?todo:?newTodo,?todos:?allTodos?})
??}?catch?(error)?{
????throw?error
??}
}
addTodo()??函數(shù)接收包含用戶輸入數(shù)據(jù)的 body 對(duì)象。
接下來(lái),我使用類型轉(zhuǎn)換來(lái)避免拼寫錯(cuò)誤,并限制 ?body??變量與 ?ITodo??類型匹配,然后基于該模塊創(chuàng)建一個(gè)新的 Todo。
有了這些,我們現(xiàn)在可以在 DB 中保存 Todo 并返回新增的 Todo 和更新后的 todos 數(shù)組。
controllers/todos/index.ts
const?updateTodo?=?async?(req:?Request,?res:?Response):?Promise<void>?=>?{
??try?{
????const?{
??????params:?{?id?},
??????body,
????}?=?req
????const?updateTodo:?ITodo?|?null?=?await?Todo.findByIdAndUpdate(
??????{?_id:?id?},
??????body
????)
????const?allTodos:?ITodo[]?=?await?Todo.find()
????res.status(200).json({
??????message:?"Todo?updated",
??????todo:?updateTodo,
??????todos:?allTodos,
????})
??}?catch?(error)?{
????throw?error
??}
}
req??對(duì)象中獲取 body,然后把他們傳入 ?findByIdAndUpdate(),這個(gè)函數(shù)將會(huì)在數(shù)據(jù)庫(kù)中找到 Todo 并且更新它。controllers/todos/index.ts
const?deleteTodo?=?async?(req:?Request,?res:?Response):?Promise<void>?=>?{
??try?{
????const?deletedTodo:?ITodo?|?null?=?await?Todo.findByIdAndRemove(
??????req.params.id
????)
????const?allTodos:?ITodo[]?=?await?Todo.find()
????res.status(200).json({
??????message:?"Todo?deleted",
??????todo:?deletedTodo,
??????todos:?allTodos,
????})
??}?catch?(error)?{
????throw?error
??}
}
export?{?getTodos,?addTodo,?updateTodo,?deleteTodo?}
deleteTodo()??函數(shù)允許你從數(shù)據(jù)庫(kù)中刪除 Todo。在這里,我們從 req 中拿到 id,并把它作為參數(shù)傳遞給 ?findByIdAndRemove(),來(lái)獲取到對(duì)應(yīng)的 Todo 并從 DB 中刪除它。
接下來(lái),導(dǎo)出這些函數(shù)以便我們?cè)谄渌募惺褂盟鼈儭R簿褪钦f(shuō),我們現(xiàn)在可以為 API 創(chuàng)建一些路由,并使用這些方法來(lái)處理請(qǐng)求。
創(chuàng)建 API 路由
routes/index.ts
import?{?Router?}?from?"express"
import?{?getTodos,?addTodo,?updateTodo,?deleteTodo?}?from?"../controllers/todos"
const?router:?Router?=?Router()
router.get("/todos",?getTodos)
router.post("/add-todo",?addTodo)
router.put("/edit-todo/:id",?updateTodo)
router.delete("/delete-todo/:id",?deleteTodo)
export?default?router
到目前為止,我們已經(jīng)談了很多,但是仍然沒有啟動(dòng)服務(wù)器。所以,我們?cè)谙乱还?jié)中解決這個(gè)問題。
創(chuàng)建服務(wù)器
在創(chuàng)建服務(wù)器之前,我們需要在 ?nodemon.json??加一些環(huán)境變量來(lái)保存 MongoDB 的憑據(jù)。
nodemon.json
{
????"env":?{
????????"MONGO_USER":?"your-username",
????????"MONGO_PASSWORD":?"your-password",
????????"MONGO_DB":?"your-db-name"
????}
}
app.ts
import?express,?{?Express?}?from?"express"
import?mongoose?from?"mongoose"
import?cors?from?"cors"
import?todoRoutes?from?"./routes"
const?app:?Express?=?express()
const?PORT:?string?|?number?=?process.env.PORT?||?4000
app.use(cors())
app.use(todoRoutes)
const?uri:?string?=?`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@clustertodo.raz9g.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&w=majority`
const?options?=?{?useNewUrlParser:?true,?useUnifiedTopology:?true?}
mongoose.set("useFindAndModify",?false)
mongoose
??.connect(uri,?options)
??.then(()?=>
????app.listen(PORT,?()?=>
??????console.log(`Server?running?on?http://localhost:${PORT}`)
????)
??)
??.catch(error?=>?{
????throw?error
??})
express??庫(kù)開始,這使用我們能調(diào)用 ?use()??方法,這個(gè)方法將幫助處理 Todo 路由。然后,我們用 ?mongoose??包,通過讀取 ?nodemon.json??帶憑證的 url 去連接 MongoDB。
就是說(shuō),現(xiàn)在如果我們能成功連接 MongoDB,服務(wù)器就會(huì)啟動(dòng),否則,會(huì)拋出錯(cuò)誤。
我們現(xiàn)在已經(jīng)通過 Node、Express、TypeScript 和 MongoDB 完成 api 的構(gòu)建。現(xiàn)在我們開始用 React 和 TypeScript 構(gòu)建客戶端。
用 React 和 TypeScript 創(chuàng)建客戶端
構(gòu)建
為了創(chuàng)建一個(gè)新的 React 應(yīng)用,我將會(huì)使用 create-react-app ——你可以用其他你想用的方法。
所以,在終端運(yùn)行以下代碼:
npx?create-react-app?my-app?--template?typescript
然后,為了能獲取遠(yuǎn)程數(shù)據(jù)安裝 Axios 庫(kù)。
??yarn?add?axios安裝完成后,按照以下目錄構(gòu)建項(xiàng)目:
├──?node_modules
├──?public
├──?src
|??├──?API.ts
|??├──?App.test.tsx
|??├──?App.tsx
|??├──?components
|??|??├──?AddTodo.tsx
|??|??└──?TodoItem.tsx
|??├──?index.css
|??├──?index.tsx
|??├──?react-app-env.d.ts
|??├──?setupTests.ts
|??└──?type.d.ts
├──?tsconfig.json
├──?package.json
└──?yarn.lock
這樣,我們有一個(gè)相對(duì)簡(jiǎn)單的文件結(jié)構(gòu)。最值得注意的是 ?src/type.d.ts??被用來(lái)存放類型。我?guī)缀踉诿總€(gè)文件中都使用了它們,所以我添加了擴(kuò)展 ?.d.ts??,使類型全局可用?,F(xiàn)在我們不再需要導(dǎo)入它們。
創(chuàng)建 Todo 類型
src/type.d.ts
interface?ITodo?{
??_id:?string
??name:?string
??description:?string
??status:?boolean
??createdAt?:?string
??updatedAt?:?string
}
interface?TodoProps?{
??todo:?ITodo
}
type?ApiDataType?=?{
??message:?string
??status:?string
??todos:?ITodo[]
??todo?:?ITodo
}
這里, ?ITodo??接口需要跟 API 返回的數(shù)據(jù)類型一樣。這里沒有 ?mongoose? , 所以需要加一些額外的屬性來(lái)匹配 API 定義的數(shù)據(jù)類型。
然后,我們用相同的的接口定義 ?TodoProps??,組件會(huì)接受它并渲染數(shù)據(jù)。
現(xiàn)在我們已經(jīng)定義了類型——現(xiàn)在讓我們開始從 API 獲取數(shù)據(jù)。
從API獲取數(shù)據(jù)
src/API.ts
import?axios,?{?AxiosResponse?}?from?"axios"
const?baseUrl:?string?=?"http://localhost:4000"
export?const?getTodos?=?async?():?Promise>?=>?{
??try?{
????const?todos:?AxiosResponse?=?await?axios.get(
??????baseUrl?+?"/todos"
????)
????return?todos
??}?catch?(error)?{
????throw?new?Error(error)
??}
}
axios,通過 api 來(lái)請(qǐng)求數(shù)據(jù),然后,用 ?getTodos()??函數(shù)從服務(wù)端獲取數(shù)據(jù)。它將返回 ?AxiosResponse??為類型的 promise, 保存獲取到的 ?ApiDataType??類型的 Todos。src/API.ts
export?const?addTodo?=?async?(
??formData:?ITodo
):?Promise>?=>?{
??try?{
????const?todo:?Omit"_id">?=?{
??????name:?formData.name,
??????description:?formData.description,
??????status:?false,
????}
????const?saveTodo:?AxiosResponse?=?await?axios.post(
??????baseUrl?+?"/add-todo",
??????todo
????)
????return?saveTodo
??}?catch?(error)?{
????throw?new?Error(error)
??}
}
這個(gè)函數(shù)接受用戶輸入的數(shù)據(jù)作為參數(shù)并返回 promise。這里,我們需要去掉 ?_id??屬性因?yàn)?MongoDB 會(huì)自動(dòng)生成。
src/API.ts
export?const?updateTodo?=?async?(
??todo:?ITodo
):?Promise>?=>?{
??try?{
????const?todoUpdate:?Pick"status">?=?{
??????status:?true,
????}
????const?updatedTodo:?AxiosResponse?=?await?axios.put(
??????`${baseUrl}/edit-todo/${todo._id}`,
??????todoUpdate
????)
????return?updatedTodo
??}?catch?(error)?{
????throw?new?Error(error)
??}
}
狀態(tài)??,那么在發(fā)送到服務(wù)器之前我們只需要選擇所需的屬性即可。src/API.ts
export?const?deleteTodo?=?async?(
??_id:?string
):?Promise>?=>?{
??try?{
????const?deletedTodo:?AxiosResponse?=?await?axios.delete(
??????`${baseUrl}/delete-todo/${_id}`
????)
????return?deletedTodo
??}?catch?(error)?{
????throw?new?Error(error)
??}
}
_id??屬性作為參數(shù)并返回 promise。有了這些,我們現(xiàn)在可以轉(zhuǎn)到 components 文件夾并向其文件中添加一些有意義的代碼。
創(chuàng)建組件
增加 Todo 表單
components/AddTodo.tsx
import?React?from?"react"
type?Props?=?TodoProps?&?{
??updateTodo:?(todo:?ITodo)?=>?void
??deleteTodo:?(_id:?string)?=>?void
}
const?Todo:?React.FC?=?({?todo,?updateTodo,?deleteTodo?})?=>?{
??const?checkTodo:?string?=?todo.status???`line-through`?:?""
??return?(
????<div?className="Card">
??????<div?className="Card--text">
????????<h1?className={checkTodo}>{todo.name}h1>
????????<span?className={checkTodo}>{todo.description}span>
??????div>
??????<div?className="Card--button">
????????<button
??????????onClick={()?=>?updateTodo(todo)}
??????????className={todo.status???`hide-button`?:?"Card--button__done"}
????????>
??????????Complete
????????button>
????????<button
??????????onClick={()?=>?deleteTodo(todo._id)}
??????????className="Card--button__delete"
????????>
??????????Delete
????????button>
??????div>
????div>
??)
}
export?default?Todo
saveTodo()??方法為 props,該方法允許我們將數(shù)據(jù)保存到數(shù)據(jù)庫(kù)。然后,我們創(chuàng)建 ?formData? state,它需要匹配 ITodo 類型來(lái)滿足編譯器的要求。這就是我們將它傳遞給 useState hook 的原因。我們還需要添加一個(gè)替代類型({}),因?yàn)槌跏紶顟B(tài)是個(gè)空對(duì)象。
有了這些,我們現(xiàn)在可以繼續(xù)下一步,展示獲取的數(shù)據(jù)。
展示 Todo
components/TodoItem.tsx
import?React?from?"react"
type?Props?=?TodoProps?&?{
??updateTodo:?(todo:?ITodo)?=>?void
??deleteTodo:?(_id:?string)?=>?void
}
const?Todo:?React.FC?=?({?todo,?updateTodo,?deleteTodo?})?=>?{
??const?checkTodo:?string?=?todo.status???`line-through`?:?""
??return?(
????<div?className="Card">
??????<div?className="Card--text">
????????<h1?className={checkTodo}>{todo.name}h1>
????????<span?className={checkTodo}>{todo.description}span>
??????div>
??????<div?className="Card--button">
????????<button
??????????onClick={()?=>?updateTodo(todo)}
??????????className={todo.status???`hide-button`?:?"Card--button__done"}
????????>
??????????Complete
????????button>
????????<button
??????????onClick={()?=>?deleteTodo(todo._id)}
??????????className="Card--button__delete"
????????>
??????????Delete
????????button>
??????div>
????div>
??)
}
export?default?Todo
TodoProps??類型并加入 ?updateTodo??和 ?deleteTodo??函數(shù),作為 props 傳遞給組件。現(xiàn)在,當(dāng)傳入 Todo 對(duì)象,我們將能夠顯示它并更新或刪除 Todo。
太棒了!現(xiàn)在我們可以到 ?App.tsx??文件并把最后一塊拼圖放進(jìn)去。
獲取和展示數(shù)據(jù)
App.tsx
import?React,?{?useEffect,?useState?}?from?'react'
import?TodoItem?from?'./components/TodoItem'
import?AddTodo?from?'./components/AddTodo'
import?{?getTodos,?addTodo,?updateTodo,?deleteTodo?}?from?'./API'
const?App:?React.FC?=?()?=>?{
??const?[todos,?setTodos]?=?useState([])
??useEffect(()?=>?{
????fetchTodos()
??},?[])
??const?fetchTodos?=?():?void?=>?{
????getTodos()
????.then(({?data:?{?todos?}?}:?ITodo[]?|?any)?=>?setTodos(todos))
????.catch((err:?Error)?=>?console.log(err))
??}
API.ts??導(dǎo)出的函數(shù)。然后,我們傳遞 ?ITodo??類型的數(shù)組給 ?useState??并且把它初始化為空數(shù)組。getTodos()??方法會(huì)返回 promise —— 因此,我們可以調(diào)用 then 函數(shù)并用獲取到的數(shù)據(jù)更新 state,或者在發(fā)生任何錯(cuò)誤時(shí)拋出一個(gè)錯(cuò)誤。
有了這些,我們現(xiàn)在可以在組件組件成功掛載之后,調(diào)用 ?fetchTodos()??函數(shù)。
App.tsx
const?handleSaveTodo?=?(e:?React.FormEvent,?formData:?ITodo):?void?=>?{
??e.preventDefault()
??addTodo(formData)
????.then(({?status,?data?})?=>?{
??????if?(status?!==?201)?{
????????throw?new?Error("Error!?Todo?not?saved")
??????}
??????setTodos(data.todos)
????})
????.catch(err?=>?console.log(err))
}
addTodo()??向服務(wù)端發(fā)送請(qǐng)求。如果 Todo 被成功保存,我們將更新數(shù)據(jù),否則將會(huì)拋出錯(cuò)誤。App.tsx
const?handleUpdateTodo?=?(todo:?ITodo):?void?=>?{
??updateTodo(todo)
????.then(({?status,?data?})?=>?{
??????if?(status?!==?200)?{
????????throw?new?Error("Error!?Todo?not?updated")
??????}
??????setTodos(data.todos)
????})
????.catch(err?=>?console.log(err))
}
const?handleDeleteTodo?=?(_id:?string):?void?=>?{
??deleteTodo(_id)
????.then(({?status,?data?})?=>?{
??????if?(status?!==?200)?{
????????throw?new?Error("Error!?Todo?not?deleted")
??????}
??????setTodos(data.todos)
????})
????.catch(err?=>?console.log(err))
}
App.tsx
return?(
????
??????My?Todos
??????
??????{todos.map((todo:?ITodo)?=>?(
??????????????????key={todo._id}
??????????updateTodo={handleUpdateTodo}
??????????deleteTodo={handleDeleteTodo}
??????????todo={todo}
????????/>
??????))}
????
??)
}
export?default?App
todos??數(shù)組并將所需的數(shù)據(jù)傳遞給 ?TodoItem。現(xiàn)在,如果你打開服務(wù)器端應(yīng)用程序的文件夾(并在終端中執(zhí)行以下命令):
yarn?start
在客戶端也如此:
yarn?start
你應(yīng)該能看到我們的 Todo 應(yīng)用程序會(huì)按預(yù)期工作。

太棒了!最后,我們使用 TypeScript、React、NodeJs、Express 和 MongoDB 完成了一個(gè) Todo 應(yīng)用程序的構(gòu)建。
附上源代碼。
謝謝閱讀!
原文鏈接:https://www.freecodecamp.org/news/how-to-build-a-todo-app-with-react-typescript-nodejs-and-mongodb/
作者:Ibrahima Ndaw
譯者:cyan.wu
