Web Avançado

Chrysthian Simão

Backend e Banco

Web Avançado

Construir Soluções Web

 

  • Frontend com React e TS
     
  • Backend com Node
     
  • Integração com Banco de dados NOSQL MongoDB

Javascript Moderno

O que queremos dizer com javascript moderno?


Sintaxe de código que funciona em todos os navegadores atuais.

95% dos navegadores utilizados hoje são: Chrome, Firefox, Edge, Safari e Opera

Javascript Moderno

Javascript tem versões e elas tem novas features a cada uma, então vamos nos basear em uma que tenha um suporte legal do que consideramos sintaxe moderna, tendo um suporte adequado

Montando Ambiente

Express

Já temos o node.js, já aprendemos a criar projetos frontend com ele, agora vamos precisar criar uma aplicação backend.

 

É um novo projeto, que vai ser levantando em uma porta separada usando uma biblioteca chamada express.js

Express

Express usa JS (ou TS no nosso caso) e isso facilita a barreira de linguagens, não é preciso aprender uma NOVA linguagem para mexer no back e no front, e só isso já ajuda grandão a diminuir a curva de aprendizado.

Express

Vamos criar um projeto usando o npm, MAS não temos o template de uma aplicação como o create-react-app ou create-next-app, vamos compor a solução manualmente.

Express

npm init

Pacotes e dependências

npm install express mongoose nodemon dotenv
  • express: será a base para nossa aplicação backend
     
  • mongoose: será a biblioteca para conectarmos no BD
     
  • nodemon: irá recarregar a nossa aplicação quando os arquivos mudarem
     
  • dotenv: será responsável por guardar e acessar as nossas variáveis de ambiente (informações necessárias para rodar nossa aplicação)

Pacotes e dependências

npm install -D typescript @types/express @types/node
  • typescript: nosso companheiro de tipagem forte nos projetos
     
  • @types: para que possamos usar as bibliotecas sem receber um xingão na IDE com TS, precisamos adicionar os tipos das bibliotecas que estamos utilizando

-D ou -dev significa instalar as dependências do projeto apenas para desenvolvimento, não são bibliotecas necessárias para a aplicação final, como o TS transpila para JS no build final, só precisamos em ambiente de dev

Configurando o TS

npx tsc --init

Todo projeto TS usa um arquivo de configurações para definir várias coisas.

O arquivo tsconfig.json define esses parâmetros e oferece flexibilidade para modificar a customizar o transpiler para as suas necessidades.

 

Esse arquivo normalmente fica na raiz do projeto.

Configurando o TS

{
  "compilerOptions": {
    "outDir": "./build",
    "target": "es2017",
    "module": "commonjs",
    "rootDir": "./",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

Configurando o TS

Se tentarmos executar um arquivo index.ts usando Node, nós vamos encontrar um erro.

 

Aplicações Node originalmente foram feitas pra usar JS, não TS!

 

Mas uma dependência adicional resolve isso pra gente

npm i -D ts-node

Package.json

Para adicionar comandos de:
 

  • build (transpile pra versão em JS)
     
  • iniciar ambiente de desenvolvimento
     
  • iniciar a aplicação final

 

Vamos adicionar uma configuração no nosso package.json

Configurando o Package.json

tsc é Typescript compiler, ele vai ler seu tsconfig.json e gerar uma versão final para deploy

  "scripts": {
    "build": "npx tsc",
    "start": "node build/index.js",
    "dev": "nodemon src/index.ts"
  },

Configurando o Nodemon

Vamos tomer um passo extra para um setup  ligeiramente mais refinado, vamos usar um  arquivo nodemon.json na raiz do projeto.

Este arquivo especifica diretórios e extensões a observar e definir comandos a serem executados, enquanto o nodemon faz sua mágica de recarregar a aplicação.

{
  "watch": ["src"],
  "ext": "ts",
  "exec": "concurrently \"npx tsc --watch\" \"ts-node src/index.ts\""
}

Configurando o Nodemon

Mas claro que isso vai pedir uma deppendência extra como você pode ter imaginado ao ver o comando

npm i -D concurrently

concurrently: vai permitir rodar comandos em paralelo

Vamos usar isso porque tanto o nodemon quanto o TSC vão monitorar alterações e oitencialmente competir para exibir seus respectivos logs.

Configurando o .env

PORT=4000

Na raiz do projeto crie um arquivo chamado ".env", esse cara vai guardar as nossas variáveis de ambiente, configurações globais da nossa aplicação, e vamos armazenar por hora, apenas a porta que a aplicação vai subir (4000)

Aplicação básica

import express, { Express, Request, Response } from "express";
import dotenv from "dotenv";

dotenv.config();

const app: Express = express();
const port = process.env.PORT || 4000;

app.get("/", (req: Request, res: Response) => {
  res.send("Express + TypeScript Server");
});

app.listen(port, () => {
  console.log(`[server]: Server is running at http://localhost:${port}`);
});

index.ts

Press Play!

Ok, configuramos tudão, mas vamos rodar isso então

npm run dev
npm run build
npm start

Executar em modo dev (com TS)
Gerar um build para produção
Executar a versão gerada no build

API

Get, Post, Put, Delete

Middlewares

Express é um framework que executa funções (middlewares) que tratam as informações que trafegam nas suas rotas.

 

Essas funções tem acesso aos objetos de Request e Response e podem fazer alterações neles

Middlewares

Para tratar  o corpo das requisições vamos usar o express.json.

Para servir arquivos estáticos como imagens, arquivos CSS, JS, use o middleware express.static.
 

Os dois já acompanham a biblioteca.

Middlewares

const app: Express = express();

app.use(express.json());
app.use(express.static("public"));

index.ts

Swagger

Esse deve ser um velho conhecido de vocês para expor endpoints do servidor de aplicação para consultar no frontend

Swagger

O que precisa pra adicionar ele?
Sim! mais dependências no projeto

npm install tsoa swagger-ui-express
npm install -D @types/swagger-ui-express
  • tsoa: Typescript Open API
     
  • @types: lembra deles? sempre inclua os tipos pra não tomar xingão no TS!

Configurando o TS (again!)

{
  "compilerOptions": {
    ...,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Adicionar suporte a decorators (swagger)

Configurando o tsoa

{
  "entryFile": "src/index.ts",
  "noImplicitAdditionalProperties": "throw-on-extras",
  "spec": {
    "outputDirectory": "public",
    "specVersion": 3
  }
}

Percebe o padrão aqui? bibliotecas novas podem precisar de configurações novas

 

Crie um tsoa.json na raiz do projeto

Criando um controller

import { Get, Route } from "tsoa";

interface PingResponse {
  message: string;
}

@Route("api/ping")
export default class PingController {
  @Get("/")
  public async getMessage(): Promise<PingResponse> {
    return {
      message: "pong!",
    };
  }
}

Criando uma rota

import express, { Request, Response } from "express";
import PingController from "../controllers/PingController";
const router = express.Router()

router.get('/ping', async (req: Request, res: Response) => {
    const controller = new PingController();
    const response = await controller.getMessage();
    return res.send(response);
})

export default router;

Middlewares (again!)

import pingRoutes from './routes/pingRoutes'
app.use('/api/', pingRoutes)

index.ts

Middlewares (again!)

app.use(
    "/swagger", /* endereço do swagger */
    swaggerUi.serve,
    swaggerUi.setup(undefined, {
      swaggerOptions: {
        url: "/swagger.json",
      },
    })
  );

index.ts

Configurando o Package.json (again!)

Swagger joins the party!

  "scripts": {
    "build": "npx tsc",
    "start": "node build/index.js",
    "dev": "nodemon src/index.ts",
    "swagger": "tsoa spec"
  },

Configurando o Nodemon

(again!)

{
  "watch": ["src"],
  "ext": "ts",
  "exec": "concurrently \"npx tsc --watch\" \"ts-node src/index.ts\" \"tsoa spec\""
}

Promises

Promises

Um objeto de Promise representaum código que irá executar asíncronamente e retornar seu valor de sucesso ou falha

const myPromise = new Promise((resolve, reject) => {
  // Asynchronous code here
});

O construtor de uma Promise aceita uma função que deve receber dois parâmetros: uma função de sucesso (resolve) e uma função de falha (reject)

Promises

myPromise
  .then((value) => {
    console.log('Promise resolved with value: ' + value);
  })
  .catch((error) => {
    console.error('Promise rejected with error: ' + error);
  });

Uma vez declarada, use os métodos .then() e .catch() para definir sucesso ou falha da operação assíncrona

Promises + async/await

async function myAsyncFunction() {
  try {
    const value = await myPromise;
    console.log('Promise resolved with value: ' + value);
  } catch (error) {
    console.error('Promise rejected with error: ' + error);
  }
}

Banco de Dados

NoSQL

É uma abordagem de banco de dados que permite armazenamento e pesquisa (Queries) fora da estrutura tradicional encontrada em bancos relacionais.

 

Existem várias abordagens e tipos de bancos NoSQL

 SQL vs NoSQL

Estrutura relacional
(tabelas, Ids, PKs, FKs, consistência, segurança de dados, backup seguro)

Estrutura não relacional
(posso definir as relações entre os dados on the fly, alta escala, alta flexibilidade)

Escala vertical
(mais poder de processamento para gerenciar relacionamentos)

Escala horizontal
(mais memória em uma escala mais eficiente que um banco relacional)

Baseado em tabelas

Baseado em documentos

( key/value, JSON etc...)

 SQL vs NoSQL

SQL

Chaos I am your master!

(cada solução pode ter a sua forma de trafegar informação)

Controle de transações com múltiplas linhas

Não Estruturado

Qual é Melhor?

Depende da sua aplicação, são soluções para problemas diferentes, é comum usar alguma combinação dos dois para uma aplicação complexa.

Qual é Melhor?

Use um banco relacional se as palavras chave são:

  • Consistência
  • Segurança
  • Facilidade de backup/restore

 

Use um NoSQL ee as palavras chave são:

  • Flexibilidade
  • Escalabilidade
  • Eficiência custo/benefício

NoSQL em ação

JSON

{
  "id": 1,
  "name": "CH",
  "role": "Frontend Engineer"
}

Muitos de nós já usamos o formato para representar alguma estrutura, já trafegamos isso entre requisições e tudo mais, e se a gente resolvesse armazenar os dados nesse formato?

JSON

Isso traz um modelo mais livre de dados, sem muita estrutura pré-definida, com qualquer número de campos e tipos.

Mongo DB

MongoDB é feito pra esse tipo de dado.

 

Ele armazena as informações em memória, mantendo um acesso fácil para consulta.

Mongo DB

Mas com grandes poderes vem... Um mapeamento louco no seu banco de dados, é bom manter alguma estrutura ao trabalhar com essas informações.

Mantenha valores nulos em campos vazios (ao invés de omití-los) e defina algum tipo de consistência na sua estrutura.

Mongo DB

Uma das coisas mais diferentes que podem surpreender, é que muitas soluções NoSQL tem serviços de nuvem, por exemplo: Firebase (muito usado em soluções para aplicativos) e claro, mongoDB.

Mongo DB

Vamos então criar uma conta em:
 

https://account.mongodb.com/account/login

Mongo DB

Escolha a opção grátis, configure as credenciais

        GUARDE A SUA SENHA!

 

Crie seu cluster, como sempre o nome é a coisa mais difícil eu usei "labs" pro meu caso vocês queiram copiar

.env

PORT=4000
DATABASE_URL=[string de conexão aqui]

Como agora temos a string de conexão, vamos adicioná-la ao seu projeto, lembre-se que ela não vem configurada com meu usuário, coloque o seu!

Criando uma conexão com o BD

import mongoose from "mongoose";

export const connect = (databaseUrl: string) =>{
    mongoose.connect(databaseUrl)
    const database = mongoose.connection
    
    database.on('error', (error) => {
      console.log(error)
    })
    
    database.once('connected', () => {
      console.log('Database Connected');
    })    
}

Criando uma conexão com o BD

import { connect } from "./service/database"

const databaseUrl = process.env.DATABASE_URL || ""
connect(databaseUrl)

Model

import mongoose from "mongoose"

const icecreamSchema = new mongoose.Schema({
  name: {
    required: true,
    type: String,
  },
})

export const IcecreamModel = 
      mongoose.model("Icecream", icecreamSchema)

Controller

  @Post("/create")
  public async create(@Body() body: { name: string })
  	: Promise<string> {
    
      const data = new IcecreamModel({
      	name: body.name,
	  })

      try {
        await data.save()
        return "OK"
      } catch (error) {
        return JSON.stringify(error)
      }
  }

Route

router.post("/create", 
            async (req: Request, res: Response) => {
  const response = await controller.create(req.body)

  return res.status(
    response === "OK" ? 200 : 400
  ).send(response)
})

Hello-Express-Mongo

Exemplos estão nesse projeto no repositório
MAS, a string de conexão com o banco é a seu critério!

Queries mais complexas

await Character.create([
  { name: 'Jean-Luc Picard', age: 59, rank: 'Captain' },
  { name: 'William Riker', age: 29, rank: 'Commander' },
  { name: 'Deanna Troi', age: 28, rank: 'Lieutenant Commander' },
]);

const filter = { age: { $gte: 30 } };
docs = await Character.find(filter);

docs.length; // 1
docs[0].name; // 'Jean-Luc Picard'
docs[0].age // 59

Queries mais complexas

  • $eq: comparar valores iguais
  • $lt: comparar valores menores
  • $lte: comparar valores menores ou iguais
  • $gt: comparar valores maiores
  • $gte: comparar valores menores ou iguais

Relacionamentos

Embora a gente possa ter relacionamentos semelhantes ao SQL, o NoSQL é mais livre, mais "freestyle", você pode criar os relacionamentos mas cabe a você reforçar as regras entre as entidades.

One-to-One

One-to-Many

Many-to-Many

Queries mais complexas

  @Get("/query")
  public async query(): Promise<JsonObject> {
    try {
      const data = await IcecreamModel.find()
        .populate("toppingId");

      return data;
    }
    catch (error: any) {
      return {
        error: error.message
      };
    }
  }

Queries mais complexas

  @Get("/fields")
  public async fields(): Promise<JsonObject> {
    try {
      const data = await IcecreamModel.find()
        .select("name toppingId -_id");

      return data;
    }
    catch (error: any) {
      return {
        error: error.message
      };
    }
  }

campos

Omita o campo _Id
(sinal de menos)

Expondo endpoints

CORS

Achou que acabaram as bibliotecas para o nosso backend? achou errado!

npm install cors
npm install -D @types/cors
  • cors: Cross-Origin Resource Sharing
     
  • @types: lembra deles?

CORS

import cors from "cors"

// aceitar requisições desse endereço
const corsOptions = { 
  origin : ['http://localhost:3000'], 
} 
 
app.use(cors(corsOptions)) 

Lá no frontend

  useEffect(() => {
    fetch('http://localhost:4000/api/icecream/getAll',
    {method: "GET"})
      .then((res) => res.json())
      .then((data) => {
        console.log(data);
      })
  }, [])

Dentro do seu componente (por exemplo)

  useEffect(() => {
    if (!isDark) {
      const data = {
        id: "6646818941907520577d1973",
        name: "Morango",
      }

      fetch("http://localhost:4000/api/icecream/update",
      {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
      })
        .then((res) => res.json())
        .then((data) => {
          console.log(data)
        })
    }
  }, [isDark])

Avaliação A1!

  • Frontend com React + TS chamando os endpoints
  • Aplicação node ouvindo chamadas do front e persistindo dados num banco NoSQL
  • GET / POST / PATCH / DELETE
  • Queries customizadas, igual a quantidate de integrantes (minimo 3)
  • Com controle de estado no front (Redux/Zustand)
  • O tema que você quiser, defendendo o código durante a apresentação

Web Avançado 3

By Chrysthian Simão

Web Avançado 3

  • 494