În acest tutorial, vom folosi TypeScript pe ambele părți (server și client) pentru a construi o aplicație Todo de la zero cu React, NodeJS, Express și MongoDB.
Deci, să începem prin planificarea API-ului.
- API cu NodeJS, Express, MongoDB și TypeScript
- Configurare
- Creați un tip Todo
- Creați un model Todo
- Creați controlere API
- Obțineți, adăugați, actualizați și ștergeți Todos
- Creați rute API
- Creați un server
- Partea client cu React și TypeScript
- Configurare
- Creați un tip Todo
- Obțineți date din API
- Creați componentele
- Adăugați formularul Todo
- Afișați un Todo
- Preluarea și afișarea datelor
- Resurse
Hai să ne scufundăm.
API cu NodeJS, Express, MongoDB și TypeScript
Pregătirea
Dacă sunteți nou în acest sens, puteți începe cu Un ghid practic pentru TypeScript sau Cum să construiți un API de la zero cu Node JS, Express și MongoDB pentru a profita la maximum de acest tutorial. În caz contrar, să începem.
Pentru a crea o nouă aplicație NodeJS, trebuie să rulați această comandă pe terminal:
yarn init
Va cere câteva întrebări și apoi inițializează aplicația. Puteți sări peste acesta adăugând un -y
semnalizați la comandă.
Apoi, structurați proiectul după cum urmează:
├── 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
După cum puteți vedea, această structură de fișiere este relativ simplă. dist
directorul va servi ca folder de ieșire odată ce codul a fost compilat în JavaScript simplu.
Avem și un app.ts
fișier care este punctul de intrare al serverului. Controlerele, tipurile și rutele sunt, de asemenea, în numele folderelor respective.
Acum, trebuie să configurăm tsconfig.json
pentru a ajuta compilatorul să urmeze preferințele noastre.
- 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"]
}
Aici avem patru proprietăți principale de subliniat:
outDir
: spune compilatorului să introducă codul compilat în dist/js
pliant.
rootDir
: informează TypeScript să compileze fiecare .ts
fișier situat în src
pliant.
include
: spune compilatorului să includă fișiere care se află în src
director și subdirector.
exclude
: va exclude fișierele sau folderele transmise în matrice în timpul compilării.
Acum putem instala dependențele pentru a activa TypeScript în proiect. Deoarece în mod implicit, această aplicație va utiliza JavaScript.
Există două moduri de utilizare a TypeScript într-o aplicație NodeJS. Fie la nivel local în proiect, fie la nivel global în mașina noastră. Voi opta pentru acesta din urmă pe baza preferințelor personale, dar puteți rămâne cu modul local dacă doriți și voi.
Acum, să executăm următoarea comandă pe terminal pentru a instala TypeScript.
yarn add typescript -g
Acest g
flag permite instalarea TypeScript la nivel global și acest lucru îl face accesibil de oriunde de pe computer.
În continuare, să adăugăm câteva dependențe pentru a utiliza Express și MongoDB.
yarn add express cors mongoose
De asemenea, trebuie să le instalăm tipurile ca dependențe de dezvoltare pentru a ajuta compilatorul TypeScript să înțeleagă pachetele.
yarn add -D @types/node @types/express @types/mongoose @types/cors
Acum, TypeScript nu vă va mai striga – va folosi aceste tipuri pentru a defini bibliotecile pe care tocmai le-am instalat.
De asemenea, trebuie să adăugăm alte dependențe pentru a putea compila codul TypeScript și a porni serverul simultan.
yarn add -D concurrently nodemon
Având în vedere acest lucru, acum putem actualiza fișierul package.json
fișier cu scripturile necesare pentru a porni serverul.
- pachet.json
"scripts": {
"build": "tsc",
"start": "concurrently "tsc -w" "nodemon dist/js/app.js""
}
concurrently
vă va ajuta să compilați codul TypeScript, să urmăriți în continuare modificările și, de asemenea, să porniți serverul simultan. Acestea fiind spuse, acum putem lansa serverul – cu toate acestea, nu am creat încă ceva semnificativ în această privință. Deci, haideți să reparăm acest lucru în secțiunea următoare.
Creați un tip Todo
- types / todo.ts
import { Document } from "mongoose"
export interface ITodo extends Document {
name: string
description: string
status: boolean
}
Aici avem o interfață Todo care extinde Document
tip furnizat de mongoose
. Îl vom folosi mai târziu pentru a interacționa cu MongoDB. Acestea fiind spuse, putem defini acum cum ar trebui să arate un model Todo.
Creați un model Todo
- modele / 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<ITodo>("Todo", todoSchema)
După cum puteți vedea aici, începem prin importarea interfeței ITodo
și câteva utilități de la mongoose
. Acesta din urmă ajută la definirea schemei Todo și, de asemenea, să treacă ITodo
ca tip la model
înainte de a-l exporta.
Cu aceasta, putem folosi acum modelul Todo în alte fișiere pentru a interacționa cu baza de date.
Creați controlere API
Obțineți, adăugați, actualizați și ștergeți 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
}
}
Aici, mai întâi trebuie să importăm câteva tipuri din express
pentru că vreau să introduc valorile în mod explicit. Dacă doriți, puteți lăsa TypeScript să o deducă.
Apoi, folosim funcția getTodos()
pentru a prelua date. Primește un req
și res
parametru și returnează o promisiune.
Și cu ajutorul Todo
model creat mai devreme, acum putem obține date de la MongoDB și putem returna un răspuns cu matricea de todos.
- controllers / todos / index.ts
const addTodo = async (req: Request, res: Response): Promise<void> => {
try {
const body = req.body as Pick<ITodo, "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
}
}
După cum puteți vedea, funcția addTodo()
primește obiectul corp care conține date introduse de utilizator.
Apoi, folosesc tipecastarea pentru a evita greșelile de tipar și pentru a restricționa body
variabilă pentru a se potrivi ITodo
și apoi creați un nou Todo bazat pe model.
Având în vedere acest lucru, acum putem salva Todo în DB și putem returna un răspuns care conține totul creat și matricea todos actualizată.
- 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
}
}
Pentru a actualiza un lucru, trebuie să extragem ID-ul și corpul din req
obiect și apoi treceți-le la findByIdAndUpdate()
. Acest utilitar va găsi Todo în baza de date și îl va actualiza. Și odată ce operațiunea este finalizată, acum putem returna datele actualizate utilizatorului.
- 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 }
Functia deleteTodo()
vă permite să ștergeți un Todo din baza de date. Aici, scoatem id-ul din req și îl transmitem ca argument către findByIdAndRemove()
pentru a accesa Todo-ul corespunzător și a-l șterge din DB.
Apoi, exportăm funcțiile pentru a le putea folosi în alte fișiere. Acestea fiind spuse, acum putem crea câteva rute pentru API și putem folosi aceste metode pentru a gestiona solicitările.
Creați rute API
- rute / 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
După cum puteți vedea aici, avem patru rute pentru a obține, adăuga, actualiza și șterge todos din baza de date. Și din moment ce am creat deja funcțiile, singurul lucru pe care trebuie să-l facem este să importăm metodele și să le trecem ca parametri pentru a gestiona solicitările.
Până acum, am acoperit multe. Dar încă nu avem un server pentru a începe. Deci, haideți să reparăm acest lucru în secțiunea următoare.
Creați un server
Înainte de a crea serverul, trebuie să adăugăm mai întâi câteva variabile de mediu care vor păstra acreditările MongoDB în nodemon.json
fişier.
- nodemon.json
{
"env": {
"MONGO_USER": "your-username",
"MONGO_PASSWORD": "your-password",
"MONGO_DB": "your-db-name"
}
}
Puteți obține acreditările prin crearea unui nou cluster pe Atlas MongoDB.
- 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
})
Aici, începem prin importarea fișierului express
bibliotecă care ne permite accesul la use()
metodă care ajută la gestionarea rutelor Todos.
Apoi, folosim mongoose
pachet pentru a vă conecta la MongoDB adăugând la adresa URL acreditările deținute pe nodemon.json
fişier.
Acestea fiind spuse, acum, dacă ne conectăm cu succes la MongoDB, serverul va porni. Dacă este cazul, va fi aruncată o eroare.
Acum am terminat de construit API-ul cu Node, Express, TypeScript și MongoDB. Să începem acum să construim aplicația din partea clientului cu React și TypeScript.

Partea client cu React și TypeScript
Configurare
Pentru a crea o nouă aplicație React, voi merge cu create-react-app – puteți utiliza și alte metode, dacă doriți.
Deci, să rulăm în terminal următoarea comandă:
npx create-react-app my-app --template typescript
Apoi, instalați biblioteca Axios pentru a putea prelua date la distanță.
yarn add axios
După finalizarea instalării, să ne structurăm proiectul după cum urmează:
├── 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
Aici avem o structură de fișiere relativ simplă. Principalul lucru de observat este că src/type.d.ts
va deține tipurile. Și din moment ce le voi folosi în aproape fiecare fișier, am adăugat extensia .d.ts
pentru a face tipurile disponibile la nivel global. Și acum nu mai trebuie să le importăm.
Creați un tip 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
}
Aici ITodo
interfața trebuie să reflecte forma datelor din API. Și din moment ce nu avem mongoose
aici, trebuie să adăugăm proprietăți suplimentare pentru a se potrivi cu tipul definit în API.
Apoi, folosim aceeași interfață pentru TodoProps
care este adnotarea de tip pentru recuzita care va fi primită de componenta responsabilă cu redarea datelor.
Am definit tipurile noastre – să începem acum să preluăm date din API.
Obțineți date din API
- src / API.ts
import axios, { AxiosResponse } from "axios"
const baseUrl: string = "http://localhost:4000"
export const getTodos = async (): Promise<AxiosResponse<ApiDataType>> => {
try {
const todos: AxiosResponse<ApiDataType> = await axios.get(
baseUrl + "/todos"
)
return todos
} catch (error) {
throw new Error(error)
}
}
După cum puteți vedea, trebuie să importăm axios
pentru a solicita date de la API. Apoi, folosim funcția getTodos()
pentru a obține date de pe server. Va returna o promisiune de tip AxiosResponse
care ține toate preluate care trebuie să se potrivească cu tipul ApiDataType
.
- src / API.ts
export const addTodo = async (
formData: ITodo
): Promise<AxiosResponse<ApiDataType>> => {
try {
const todo: Omit<ITodo, "_id"> = {
name: formData.name,
description: formData.description,
status: false,
}
const saveTodo: AxiosResponse<ApiDataType> = await axios.post(
baseUrl + "/add-todo",
todo
)
return saveTodo
} catch (error) {
throw new Error(error)
}
}
Această funcție primește datele introduse de utilizator ca argument și returnează o promisiune. Aici, trebuie să omitem _id
proprietate deoarece MongoDB o va crea din mers.
- src / API.ts
export const updateTodo = async (
todo: ITodo
): Promise<AxiosResponse<ApiDataType>> => {
try {
const todoUpdate: Pick<ITodo, "status"> = {
status: true,
}
const updatedTodo: AxiosResponse<ApiDataType> = await axios.put(
`${baseUrl}/edit-todo/${todo._id}`,
todoUpdate
)
return updatedTodo
} catch (error) {
throw new Error(error)
}
}
Pentru a actualiza un Todo, trebuie să trecem datele actualizate și fișierul _id
a obiectului. Aici, trebuie să schimbăm status
din Todo, motiv pentru care aleg proprietatea de care avem nevoie doar înainte de a trimite cererea către server.
- src / API.ts
export const deleteTodo = async (
_id: string
): Promise<AxiosResponse<ApiDataType>> => {
try {
const deletedTodo: AxiosResponse<ApiDataType> = await axios.delete(
`${baseUrl}/delete-todo/${_id}`
)
return deletedTodo
} catch (error) {
throw new Error(error)
}
}
Aici avem și o funcție care primește ca parametru _id
proprietate și returnează o promisiune.
Cu asta în loc, putem merge acum la components
folder și adăugați un cod semnificativ în fișierele sale.
Creați componentele
Adăugați formularul Todo
- components / AddTodo.tsx
import React from "react"
type Props = TodoProps & {
updateTodo: (todo: ITodo) => void
deleteTodo: (_id: string) => void
}
const Todo: React.FC<Props> = ({ 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
După cum puteți vedea, aici avem o componentă funcțională de tip React.FC
(FC înseamnă componentă funcțională). Primește drept suport metoda saveTodo()
care ne permite să salvăm date în DB.
Apoi, avem un formData
stat care trebuie să se potrivească cu ITodo
tastați pentru a satisface compilatorul. De aceea îl transmitem către useState
cârlig. De asemenea, trebuie să adăugăm un tip alternativ ({}
) deoarece starea inițială va fi un obiect gol.
Și cu aceasta, acum putem merge mai departe și afișa datele preluate.
Afișați un Todo
- components / TodoItem.tsx
import React from "react"
type Props = TodoProps & {
updateTodo: (todo: ITodo) => void
deleteTodo: (_id: string) => void
}
const Todo: React.FC<Props> = ({ 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
Aici, trebuie să extindem TodoProps
tastați și adăugați funcțiile updateTodo
și deleteTodo
să manipuleze în mod adecvat recuzita primită de componentă.
Acum, odată ce obiectul Todo a trecut, îl vom putea afișa și adăuga funcțiile necesare pentru actualizarea sau ștergerea unui Todo.
Grozav! Acum putem merge la App.tsx
fișier și adăugați ultima piesă la puzzle.
Preluarea și afișarea datelor
- 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<ITodo[]>([])
useEffect(() => {
fetchTodos()
}, [])
const fetchTodos = (): void => {
getTodos()
.then(({ data: { todos } }: ITodo[] | any) => setTodos(todos))
.catch((err: Error) => console.log(err))
}
Aici, trebuie mai întâi să importăm componentele și funcțiile de utilitate deținute API.ts
. Apoi, trecem la useState
o matrice de tip ITodo
și inițializați-l cu un tablou gol.
Metoda getTodos()
returnează o promisiune – prin urmare, putem accesa fișierul then
funcționează și actualizează starea cu datele preluate sau aruncă o eroare dacă apare vreuna.
Având în vedere acest lucru, putem apela acum funcția fetchTodos()
când componenta este montată cu succes.
- 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))
}
Odată ce formularul este trimis, îl folosim addTodo()
pentru a trimite cererea către server și apoi dacă Todo a salvat cu succes, actualizăm datele, altfel va fi aruncată o eroare.
- 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))
}
Funcțiile de actualizare sau ștergere a unui Todo sunt destul de similare. Amândoi primesc un parametru, trimit cererea și primesc înapoi un răspuns. Și apoi verifică dacă solicitarea a avut succes și o gestionează în consecință.
- App.tsx
return (
<main className="App">
<h1>My Todos</h1>
<AddTodo saveTodo={handleSaveTodo} />
{todos.map((todo: ITodo) => (
<TodoItem
key={todo._id}
updateTodo={handleUpdateTodo}
deleteTodo={handleDeleteTodo}
todo={todo}
/>
))}
</main>
)
}
export default App
Aici, facem o buclă prin todos
matrice și apoi treceți la TodoItem
datele așteptate.
Acum, dacă navigați în folderul care conține aplicația de pe server (și executați următoarea comandă în terminal):
yarn start
Și, de asemenea, în aplicația din partea clientului:
yarn start
Ar trebui să vedeți că aplicația noastră Todo funcționează conform așteptărilor.

Grozav! Cu acea atingere finală, am terminat acum construirea unei aplicații Todo folosind TypeScript, React, NodeJs, Express și MongoDB.
Puteți găsi Codul sursă aici.
Puteți găsi alt conținut grozav ca acesta pe blogul meu sau urmează-mă pe Twitter pentru a fi notificat.
Mulțumesc pentru lectură.
Resurse
Foaie de trucuri pentru tipuri avansate de tipScript (cu exemple)