yarn init
Ele fará algumas perguntas e inicializará o aplicativo. Você pode ignorá-lo adicionando um -y sinalizador para o comando.
Em seguida, estruture o projeto da seguinte maneira:
├── 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
Como você pode ver, essa estrutura de arquivos é relativamente simples. o dist O diretório servirá como uma pasta de saída assim que o código for compilado para JavaScript simples.
Nós também temos um app.ts arquivo que é o ponto de entrada do servidor. Os controladores, tipos e rotas também estão em seus respectivos nomes de pastas.
Agora, precisamos configurar o tsconfig.json para ajudar o compilador a seguir nossas preferências.
{
"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"]
}
Aqui temos quatro propriedades principais a serem sublinhadas:
outDir: diz ao compilador para colocar o código compilado no diretório dist/js pasta.
rootDir: informa o TypeScript para compilar todos os .ts arquivo localizado no src pasta.
include: diz ao compilador para incluir arquivos que estão no diretório src diretório e subdiretório.
exclude: excluirá os arquivos ou pastas passados na matriz durante o tempo de compilação.
Agora podemos instalar as dependências para ativar o TypeScript no projeto. Porque, por padrão, este aplicativo usa JavaScript.
Existem duas maneiras de usar o TypeScript em um aplicativo NodeJS. Localmente no projeto ou globalmente em nossa máquina. Eu irei para o último com base na preferência pessoal, mas você pode seguir o caminho local, se quiser também.
Agora, vamos executar o seguinte comando no terminal para instalar o TypeScript.
yarn add typescript -g
este g O sinalizador permite instalar o TypeScript globalmente e isso o torna acessível de qualquer lugar do computador.
Em seguida, vamos adicionar algumas dependências para usar o Express e o MongoDB.
yarn add express cors mongoose
Também precisamos instalar seus tipos como dependências de desenvolvimento para ajudar o compilador TypeScript a entender os pacotes.
yarn add -D @types/node @types/express @types/mongoose @types/cors
Agora, o TypeScript não gritará mais com você – ele usará esses tipos para definir as bibliotecas que acabamos de instalar.
Também precisamos adicionar outras dependências para poder compilar o código TypeScript e iniciar o servidor simultaneamente.
yarn add -D concurrently nodemon
Com isso, agora podemos atualizar o package.json arquivo com os scripts necessários para iniciar o servidor.
"scripts": {
"build": "tsc",
"start": "concurrently "tsc -w" "nodemon dist/js/app.js""
}
concurrently ajudará a compilar o código TypeScript, continuará observando as alterações e também iniciará o servidor simultaneamente. Dito isto, agora podemos iniciar o servidor – no entanto, ainda não criamos algo significativo a esse respeito. Então, vamos corrigir isso na próxima seção.
Criar um Tipo Todo
import { Document } from "mongoose"
export interface ITodo extends Document {
name: string
description: string
status: boolean
}
Aqui, temos uma interface Todo que estende o Document tipo fornecido por mongoose. Nós o usaremos mais tarde para interagir com o MongoDB. Dito isso, agora podemos definir a aparência de um modelo Todo.
Criar um modelo Todo
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)
Como você pode ver aqui, começamos importando a interface ITodo e alguns utilitários de mongoose. Este último ajuda a definir o esquema Todo e também passa ITodo como um tipo para o model antes de exportá-lo.
Com isso, agora podemos usar o modelo Todo em outros arquivos para interagir com o banco de dados.
Criar controladores de API
Obter, adicionar, atualizar e excluir 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
}
}
Aqui, primeiro precisamos importar alguns tipos de express porque eu quero digitar os valores explicitamente. Se desejar, você pode permitir que o TypeScript deduza isso para você.
Em seguida, usamos a função getTodos() buscar dados. Recebe um req e res parâmetro e retorna uma promessa.
E com a ajuda do Todo modelo criado anteriormente, agora podemos obter dados do MongoDB e retornar uma resposta com a matriz 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
}
}
Como você pode ver, a função addTodo() recebe o objeto do corpo que contém os dados inseridos pelo usuário.
Em seguida, uso a tipografia para evitar erros de digitação e restringir o body variável a corresponder ITodo e crie um novo Todo baseado no modelo.
Com isso, agora podemos salvar o Todo no banco de dados e retornar uma resposta que contenha o todo criado e a matriz todos atualizada.
- 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
}
}
Para atualizar um todo, precisamos extrair o id e o corpo do req objeto e depois passá-los para findByIdAndUpdate(). Este utilitário encontrará o Todo no banco de dados e o atualizará. E depois que a operação estiver concluída, agora podemos retornar os dados atualizados para o usuário.
- 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 }
A função deleteTodo() permite excluir um Todo do banco de dados. Aqui, extraímos o id de req e o passamos como argumento para findByIdAndRemove() para acessar o Todo correspondente e excluí-lo do banco de dados.
Em seguida, exportamos as funções para poder usá-las em outros arquivos. Dito isso, agora podemos criar algumas rotas para a API e usar esses métodos para lidar com as solicitações.
Criar rotas de API
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
Como você pode ver aqui, temos quatro rotas para obter, adicionar, atualizar e excluir todos do banco de dados. E como já criamos as funções, a única coisa que precisamos fazer é importar os métodos e passá-los como parâmetros para lidar com as solicitações.
Até agora, cobrimos muito. Mas ainda não temos um servidor para iniciar. Então, vamos corrigir isso na próxima seção.
Crie um servidor
Antes de criar o servidor, precisamos primeiro adicionar algumas variáveis de ambiente que manterão as credenciais do MongoDB no diretório nodemon.json Arquivo.
{
"env": {
"MONGO_USER": "your-username",
"MONGO_PASSWORD": "your-password",
"MONGO_DB": "your-db-name"
}
}
Você pode obter as credenciais criando um novo cluster em MongoDB Atlas.
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
})
Aqui, começamos importando o express biblioteca que nos permite acessar o use() método que ajuda a lidar com as rotas Todos.
Em seguida, usamos o mongoose pacote para se conectar ao MongoDB anexando à URL as credenciais mantidas no nodemon.json Arquivo.
Dito isto, agora, se nos conectarmos com sucesso ao MongoDB, o servidor será iniciado. Se apropriado, um erro será gerado.
Agora, terminamos de criar a API com Node, Express, TypeScript e MongoDB. Vamos agora começar a criar o aplicativo do lado do cliente com React e TypeScript.
Lado do cliente com React e TypeScript
Configurando
Para criar um novo aplicativo React, irei com create-react-app – você também pode usar outros métodos, se quiser.
Então, vamos executar no terminal o seguinte comando:
npx create-react-app my-app --template typescript
Em seguida, instale a biblioteca Axios para poder buscar dados remotos.
yarn add axios
Depois que a instalação estiver concluída, vamos estruturar nosso projeto da seguinte maneira:
├── 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
Aqui, temos uma estrutura de arquivos relativamente simples. O principal a notar é que src/type.d.ts irá conter os tipos. E como eu os usarei em quase todos os arquivos, adicionei a extensão .d.ts para tornar os tipos disponíveis globalmente. E agora não precisamos mais importá-los.
Criar um Tipo Todo
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
}
Aqui o ITodo A interface precisa espelhar a forma dos dados da API. E já que não temos mongoose aqui, precisamos adicionar propriedades adicionais para corresponder ao tipo definido na API.
Em seguida, usamos a mesma interface para o TodoProps que é a anotação de tipo para os adereços que serão recebidos pelo componente responsável pela renderização dos dados.
Agora definimos nossos tipos – agora vamos começar a buscar dados da API.
Buscar dados da API
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)
}
}
Como você pode ver, precisamos importar axios para solicitar dados da API. Em seguida, usamos a função getTodos() para obter dados do servidor. Ele retornará uma promessa do tipo AxiosResponse que contém o Todos buscado que precisa corresponder ao tipo ApiDataType.
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)
}
}
Esta função recebe os dados inseridos pelo usuário como argumento e retorna uma promessa. Aqui, precisamos omitir o _id propriedade porque o MongoDB irá criá-lo rapidamente.
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)
}
}
Para atualizar um Todo, precisamos passar os dados atualizados e o _id do objeto. Aqui, precisamos mudar o status do Todo, e é por isso que só escolho a propriedade necessária antes de enviar a solicitação ao servidor.
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)
}
}
Aqui, também temos uma função que recebe como parâmetro o _id propriedade e retorna uma promessa.
Com isso, agora podemos ir para o components pasta e adicione algum código significativo aos seus arquivos.
Crie os componentes
Adicionar formulário Todo
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
Como você pode ver, aqui temos um componente funcional do tipo React.FC (FC significa componente funcional). Ele recebe como suporte o método saveTodo() isso nos permite salvar dados no banco de dados.
Em seguida, temos um formData estado que precisa corresponder ao ITodo digite para satisfazer o compilador. É por isso que passamos para o useState gancho. Também precisamos adicionar um tipo alternativo ({}) porque o estado inicial será um objeto vazio.
E com isso, agora podemos avançar e exibir os dados buscados.
Exibir um Todo
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
Aqui, precisamos estender o TodoProps digite e acrescente as funções updateTodo e deleteTodo para manipular adequadamente os objetos recebidos pelo componente.
Agora, depois que o objeto Todo for transferido, poderemos exibi-lo e adicionar as funções necessárias para atualizar ou excluir um Todo.
Ótimo! Agora podemos ir para o App.tsx arquivo e adicione a última peça ao quebra-cabeça.
Buscar e exibir dados
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))
}
Aqui, primeiro precisamos importar os componentes e funções utilitárias mantidas no API.ts. Em seguida, passamos para useState uma matriz do tipo ITodo e inicialize-o com uma matriz vazia.
O método getTodos() retorna uma promessa – portanto, podemos acessar o then funcionar e atualizar o estado com os dados buscados ou gerar um erro, se houver algum.
Com isso, agora podemos chamar a função fetchTodos() quando o componente é montado com sucesso.
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))
}
Depois que o formulário é enviado, usamos addTodo() para enviar a solicitação ao servidor e, se o Todo tiver sido salvo com êxito, atualizamos os dados; caso contrário, um erro será gerado.
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))
}
As funções para atualizar ou excluir um Todo são bastante semelhantes. Ambos recebem um parâmetro, enviam a solicitação e recebem uma resposta. E então, eles verificam se a solicitação foi bem-sucedida e a tratam adequadamente.
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
Aqui, percorremos o todos matriz e depois passar para o TodoItem os dados esperados.
Agora, se você procurar na pasta que contém o aplicativo do servidor (e executar o seguinte comando no terminal):
yarn start
E também no aplicativo do lado do cliente:
yarn start
Você deve ver que nosso aplicativo Todo funciona como esperado.
Ótimo! Com esse toque final, concluímos a criação de um aplicativo Todo usando TypeScript, React, NodeJs, Express e MongoDB.
Você pode encontrar o Código fonte aqui.
Você pode encontrar outros ótimos conteúdos como este em meu blog ou me siga no Twitter para ser notificado.
Obrigado pela leitura.
Recursos
Folha de dicas sobre tipos avançados de TypeScript (com exemplos)
