Î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.

excitat

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.

aplicație

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

React TypeScript Cheatsheet

Foaie de trucuri pentru tipuri avansate de tipScript (cu exemple)

Taste de tip TypeScript