Chrysthian Simão
Construir Soluções Web
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 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
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 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.
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.
npm initnpm install express mongoose nodemon dotenvnpm install -D typescript @types/express @types/node-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
npx tsc --initTodo 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.
{
"compilerOptions": {
"outDir": "./build",
"target": "es2017",
"module": "commonjs",
"rootDir": "./",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
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-nodePara adicionar comandos de:
Vamos adicionar uma configuração no nosso 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"
},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\""
}Mas claro que isso vai pedir uma deppendência extra como você pode ter imaginado ao ver o comando
npm i -D concurrentlyconcurrently: 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.
PORT=4000Na 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)
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
Ok, configuramos tudão, mas vamos rodar isso então
npm run dev
npm run build
npm startExecutar em modo dev (com TS)
Gerar um build para produção
Executar a versão gerada no build
Get, Post, Put, Delete
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
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.
const app: Express = express();
app.use(express.json());
app.use(express.static("public"));index.ts
Esse deve ser um velho conhecido de vocês para expor endpoints do servidor de aplicação para consultar no frontend
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{
"compilerOptions": {
...,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Adicionar suporte a decorators (swagger)
{
"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
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!",
};
}
}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;import pingRoutes from './routes/pingRoutes'
app.use('/api/', pingRoutes)index.ts
app.use(
"/swagger", /* endereço do swagger */
swaggerUi.serve,
swaggerUi.setup(undefined, {
swaggerOptions: {
url: "/swagger.json",
},
})
);
index.ts
Swagger joins the party!
"scripts": {
"build": "npx tsc",
"start": "node build/index.js",
"dev": "nodemon src/index.ts",
"swagger": "tsoa spec"
},(again!)
{
"watch": ["src"],
"ext": "ts",
"exec": "concurrently \"npx tsc --watch\" \"ts-node src/index.ts\" \"tsoa spec\""
}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)
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
async function myAsyncFunction() {
try {
const value = await myPromise;
console.log('Promise resolved with value: ' + value);
} catch (error) {
console.error('Promise rejected with error: ' + error);
}
}É 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
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
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
Depende da sua aplicação, são soluções para problemas diferentes, é comum usar alguma combinação dos dois para uma aplicação complexa.
Use um banco relacional se as palavras chave são:
Use um NoSQL ee as palavras chave são:
{
"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?
Isso traz um modelo mais livre de dados, sem muita estrutura pré-definida, com qualquer número de campos e tipos.
MongoDB é feito pra esse tipo de dado.
Ele armazena as informações em memória, mantendo um acesso fácil para consulta.
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.
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.
Vamos então criar uma conta em:
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
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!
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');
})
}
import { connect } from "./service/database"
const databaseUrl = process.env.DATABASE_URL || ""
connect(databaseUrl)import mongoose from "mongoose"
const icecreamSchema = new mongoose.Schema({
name: {
required: true,
type: String,
},
})
export const IcecreamModel =
mongoose.model("Icecream", icecreamSchema)
@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)
}
}router.post("/create",
async (req: Request, res: Response) => {
const response = await controller.create(req.body)
return res.status(
response === "OK" ? 200 : 400
).send(response)
})Exemplos estão nesse projeto no repositório
MAS, a string de conexão com o banco é a seu critério!
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 // 59Embora 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.
@Get("/query")
public async query(): Promise<JsonObject> {
try {
const data = await IcecreamModel.find()
.populate("toppingId");
return data;
}
catch (error: any) {
return {
error: error.message
};
}
} @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)
Achou que acabaram as bibliotecas para o nosso backend? achou errado!
npm install cors
npm install -D @types/corsimport cors from "cors"
// aceitar requisições desse endereço
const corsOptions = {
origin : ['http://localhost:3000'],
}
app.use(cors(corsOptions)) 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])