NestJs & Angular & Flutter

Deivid Willyan
Nerdzão/Nerdgirlz
Published in
11 min readApr 30, 2020

Olá devs tudo bem?

Hoje irei apresentar para vocês uma alternativa ao tradicional modelo fullstack de NodeJs, React e React Native. Você já ouviu falar de NestJs? Angular? e Flutter? vamos a uma breve introdução de cada uma e o seu porque da escolha.

  • NestJs: Podemos encontrar sua melhor definição no site oficial, “ Uma estrutura progressiva do Node.js. para criar aplicativos eficientes, confiáveis ​​e escaláveis ​​do lado do servidor.”, optei por escolher o Nest por conta da experiência que tenho com Java, nele utilizamos o TypeScript, este qual nos permite utilizar tipagens, interface e muito mais.
  • Angular: Aqui vale ressaltar que estaremos trabalhando com Angular 9, a uma grande confusão em torno do AngularJs e Angular 2x+, não entrarei em detalhes de porque existe essa diferença mas vale ressaltar que o novo Angular adotou o TypeScript e trouxe uma série de diferenciais para o mundo front-end, ele é um framework completo e pronto para projetos de grande porte.
  • Flutter: Diferente das tecnologias anteriores o flutter utiliza como linguagem de programação o Dart, programadores Java, C#, JS e TypeScript aqui não terão grandes dificuldades, o flutter é bem recente lançado oficialmente em dezembro de 2018, ele compila nativamente para ios, Android, (desktop e web)*, vem sendo utilizado por grandes players, minha escolha por ele foi pelo alto desempenho e grande escalabilidade, não podemos esquecer da grande comunidade no brasil (Flutterando).

Iremos criar um projeto TO DO com um CRUD completo de exemplo com o Backend em NestJs utilizando o TypeOrm e MySql, no frontend iremos utilizar o Angular 9 e mobile iremos utilizar o Flutter com o Mobx.

NestJs

Nota: Antes de tudo devemos ter o NodeJs instalado

// Instalando o NestJs
npm i -g @nestjs/cli
// Criando o projeto
nest new nest-todo
// Instalando dependências necessárias
cd nest-todo
npm install --save @nestjs/typeorm typeorm mysql
// criando estrutura do projeto
nest g module todo
nest g controller todo
nest g service todo

Muito bom agora temos a estrutura básica do nosso projeto criada, vamos começar configurando o acesso a database “todo” criada anteriormente no MySQL.

Em app.module.ts vamos declarar o import do TypeOrmModule:import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TodoModule } from './todo/todo.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'todo',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),

TodoModule,
],
controllers: [],
providers: [],
})
export class AppModule {}

Agora iremos de fato começar o desenvolvimento, criaremos a entidade que terá acesso a base da dados utilizando o TypeOrm

// todo.entity.tsimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';@Entity()
export class Todo {
constructor(desc: string) {
this.description = desc;
}
@PrimaryGeneratedColumn()
id: number;
@Column()
description: string;
@Column({ default: false })
done: boolean
}

Feito isto vamos registrar esta entity em nosso módulo local (todo.module.ts):

import { Module } from '@nestjs/common';
import { TodoService } from './todo.service';
import { TodoController } from './todo.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Todo } from './todo.entity';
@Module({
imports: [
TypeOrmModule.forFeature([Todo]),
],
providers: [TodoService],
controllers: [TodoController]
})
export class TodoModule {}

Agora iremos implementar o service, ele sera o responsável por de fato implementar nosso CRUD, ele recebera por injeção de dependência em seu construtor a instancia do repository, ela qual iremos realizar nossas chamadas

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Todo } from './todo.entity';
import { Repository } from 'typeorm';
@Injectable()
export class TodoService {
constructor(
@InjectRepository(Todo)
private repository: Repository<Todo>,

) { }
async findAll(): Promise<Todo[]> {
return await this.repository.find({ order: { id: "DESC" } });
}
async findOne(id: string): Promise<Todo> {
return await this.repository.findOne(id);
}
async save(description: string): Promise<Todo> {
return await this.repository.save(new Todo(description));
}
async update(todo: Todo): Promise<any> {
return await this.repository.update(todo.id, todo);
}
async remove(id: number): Promise<void> {
await this.repository.delete(id);
}
}

O Próximo e último passo sera implementar nosso controller, ele sera agora o responsável por criar nossa interface de comunicação rest, injetaremos via construtor o service criado anteriormente.

import { Controller, Post, Body, Get, Put, Delete, Param } from '@nestjs/common';
import { TodoService } from './todo.service';
import { Todo } from './todo.entity';
@Controller('todo')
export class TodoController {
constructor(private service: TodoService) { }@Post()
async create(@Body('description') description: string) {
return await this.service.save(description);
}
@Get()
async listAllTodos() {
return await this.service.findAll();
}
@Put()
async updateTodo(@Body() todo: Todo) {
return await this.service.update(todo);
}
@Delete(':id')
async removeTodo(@Param('id') id: number) {
return await this.service.remove(id);
}
}

Devemos notar que no código a cima o Nest utiliza os decorators(anotações) para expor os verbos http que iremos utilizar para a chamada em nosso mobile/frontend.

// colocando o servidor no arnpm run start:dev

O servidor deve estar rodando em http://localhost:3000 com o controller apontando para “/todo”.

Angular 9

Iremos começar com a instalação do angular e sua configuração inicial, logo em seguida vamos direto para o código.

// Primeiro iremos instalar o CLI que sera aqui o responsável por executar os comandos via terminal. 
npm install -g @angular/cli
// Agora iremos criar o projeto
ng new angular-todo
// Configurando o projeto
cd angular-todo
// instalando o bootstrap
npm install bootstrap --save
// no arquivo styles.css import o bootstrap de forma global
@import "~bootstrap/dist/css/bootstrap.css";
// vamos gerar os arquivos do projeto.// Component
ng g c todo
// Service
ng g s todo

Agora iremos de fato começar a codar nosso projeto frontend, lembre-se no angular o arquivo principal é o “app.module.ts” nele todos nossos componentes devem ser declarados e algumas configurações iniciais devem ser feitas, para termos acesso ao modulo Http e Forms que utilizaremos a frente.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { TodoComponent } from './todo/todo.component';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent,
TodoComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
FormsModule

],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Vamos no arquivo “ app-routing.module.ts” declarar nossa rota inicial

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { TodoComponent } from './todo/todo.component';
const routes: Routes = [
{ path: '', component: TodoComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

Agora basta alterar o “app.component.html” para ficar dinamicamente obedecendo nossa rota, utilizando o router-outlet.

<router-outlet></router-outlet>

O próximo passo sera criar o model que ira representar um TODO, criamos então o arquivo “todo.model.ts” ficando da seguinte forma.

export class Todo {
id: number;
description: string;
done: boolean;
}

Vamos agora configurar nosso service para se comunicar com o backend que criamos anteriormente, então no arquivo “todo.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Todo } from './todo.model';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class TodoService {
private URL = "http://localhost:3000";constructor(private httpClient: HttpClient) { }create(description: string): Observable<Todo> {
return this.httpClient.post<Todo>(`${this.URL}/todo`, { description });
}
update(todo: Todo): Observable<Todo[]> {
return this.httpClient.put<Todo[]>(`${this.URL}/todo`, todo);
}
delete(id: number): Observable<any> {
return this.httpClient.delete(`${this.URL}/todo/${id}`);
}
findAll(): Observable<Todo[]> {
return this.httpClient.get<Todo[]>(`${this.URL}/todo`);
}
}

Lembre-se o servidor backend deve estar no ar.

Agora iremos fornecer o nosso service que foi criado para o “todo.component.ts” para que desta forma a nossa view (page) consiga realizar os binds necessários. No arquivo “todo.component.ts

import { Component, OnInit } from '@angular/core';
import { TodoService } from './todo.service';
import { Todo } from './todo.model';
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css']
})
export class TodoComponent implements OnInit {
constructor(private service: TodoService) { this.findAll(); } todos: Todo[];
todo: Todo = new Todo();
description: string;
ngOnInit(): void { }findAll(): void {
this.service.findAll()
.subscribe(data => {
this.todos = data;
this.description = '';
});
}
done(todo: Todo) {
todo.done = true;
this.update(todo);
}
update(todo: Todo) {
this.service.update(todo)
.subscribe(_ => { this.findAll(), this.todo = new Todo() });
}
create() {
if (this.todo.description !== undefined && this.todo.description.trim() !== '') {
this.service.create(this.todo.description)
.subscribe(_ => { this.findAll(), this.todo = new Todo() });
}
}
remove(id: number) {
this.service.delete(id)
.subscribe(_ => this.findAll());
}
prepareUpdate(todo: Todo) {
this.todo = todo;
}
}

Note que o service esta sendo injetado no construtor da classe, ele sera responsável por repassar as acoes do usuário para nosso backend.

Por ultimo iremos criar a tela do nosso todo, iremos utilizar agora html, bootstrap e um pouco de css

<div class="container mt-5">
<h1><i>Todo</i></h1>
<hr>
<div class="input-group mb-3 mt-5">
<input type="text" class="form-control" [(ngModel)]="todo.description" placeholder="Todo description">
<div class="input-group-append">
<button class="btn btn-outline-success" (click)="todo.id == undefined ? create() : update(todo)" type="button">Save</button>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
Todo List
<button style="float:right" (click)="findAll()" class="btn btn-sm btn-outline-info">Refresh</button>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Description</th>
<th scope="col">Options</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let todo of todos">
<th [ngStyle]="{'text-decoration-line': todo.done ? 'line-through' : ''}" scope="row">{{todo.id}}</th>
<td [ngStyle]="{'text-decoration-line': todo.done ? 'line-through' : ''}">{{todo.description}}</td>
<td>
<button [hidden]="todo.done" (click)="done(todo)" class="btn btn-sm btn-primary">Done</button>
<button [hidden]="todo.done" (click)="remove(todo.id)" style="margin-left: 5px;" class="btn btn-sm btn-danger">Delete</button>
<button [hidden]="todo.done" (click)="prepareUpdate(todo)" style="margin-left: 5px;" class="btn btn-sm btn-secondary">Update</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

No final você deve ter algo mais ou menos igual a figura a baixo:

Bom terminamos nosso backend e frontend utilizamos duas tecnologias incríveis, acredito que perceberam como o angular e o nestjs são parecidos, sabe me dizer o porque? … o NestJs foi em grande parte inspirado no Angular, trazendo ao desenvolvedor uma forma de criar o código o mais semelhante possível tanto no backend quanto no frontend, agora nosso ultimo passo sera criar nosso mobile utilizando flutter, com ele teremos as mesmas acoes feitas no angular, como inserir um todo, editar, excluir e marcar como feito, vale lembrar que agora não iremos utilizar o TypeScript e sim o dart, em grande parte são muito parecidos, acredito que vocês não terão dificuldades aqui.

Flutter

A configuração do flutter é um pouco mais chata comparada aos anteriores, precisamos seguir uma serie de passos.

// Instalando a SDK do flutter
git clone https://github.com/flutter/flutter.git -b stable
// Agora é altamente recomendado adicionar a paste bin que fica dentro da flutter que acabamos de clonar nas variáveis de ambiente da sua maquina, porem irei pular esta etapa visando correr com o exemplo, nas referencias deixarei o link de instalação completo com esta opção.// Configurando
cd flutter
flutter doctor
// ao rodar o comando "flutter doctor" seremos informados de uma serie de requezitos que vemos terminar para ter o ambiente configurado, estou utilizando um macOs e ja tenho o xcode instalado e configurado, para que utiliza o windows instale o Android Studio e um emulador de sua preferencia.// Documentacao oficial de instalacao
https://flutter.dev/docs/get-started/install/windows

Apos a instalação e configuração de todo o ambiente vamos criar nosso projeto e começar a configurar.

// em seu terminal, vamos criar o projeto
flutter create flutter_todo

Dentro do projeto iremos agora no “pubspec.yaml” e configurar as dependências do mobx que iremos utilizar:

...dependencies:
flutter:
sdk: flutter
http:
mobx:
flutter_mobx:

cupertino_icons: ^0.1.2
dev_dependencies:
flutter_test:
sdk: flutter
mobx_codegen:
build_runner:
...

Nota: Atente-se aqui a indentação, o yaml utiliza espaços ou tabs, conforme configurado em sua ide.

Agora executamos no terminal o comando “flutter pub get” ele é o responsável por disponibilizar as dependências configuradas anteriormente, agora iremos de fato a codificação.

Criaremos o projeto com a seguinte organização:

lib/
- main.dar
- - todo
- - - todo.model.dart
- - - todo.service.dart
- - - todo.controller.dart
- - - todo.page.dart

Nota: poderíamos utilizar o slidy como cli, mas por questões didáticas iremos criar tudo na mão.

Com tudo criado o primeiro passo sera alterar no main.dart para apontar para nossa page quando for feito a inicialização da main.

import 'package:flutter/material.dart';
import 'package:flutter_todo/todo/todo.page.dart';
void main() => runApp(MyApp());class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.dark
),
home: TodoPage(),
);
}
}

Configurado isto vamos codar nosso model “todo.model.dart” , ele sera uma representação do objeto todo.

class Todo {
String description;
int id;
bool done;
Todo({this.description, this.id, this.done});Todo.fromJson(Map<String, dynamic> json) {
description = json['description'];
id = json['id'];
done = json['done'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['description'] = this.description;
data['id'] = this.id;
data['done'] = this.done;
return data;
}
}

O próximo passo sera realizar a implementação do nosso “todo.service.dart”, lembrando que ele sera o responsável por se comunicar com o backend.

import 'package:flutter_todo/todo/todo.model.dart';import 'package:http/http.dart' as http;
import 'dart:convert' as convert;
class TodoService {
String URL = 'http://localhost:3000/todo';
Future<List<Todo>> listAll() async {
http.Response response = await http.get(URL);
if (response.statusCode == 200 || response.statusCode == 201) {
Iterable list = convert.json.decode(response.body);
return list.map((i) => Todo.fromJson(i)).toList();
}
}
Future<bool> create(String description) async {
http.Response response =
await http.post(URL, body: {'description': description});
return (response.statusCode == 200 || response.statusCode == 201);
}
Future<bool> remove(int id) async {
http.Response response = await http.delete('${URL}/${id}');
return (response.statusCode == 200 || response.statusCode == 201);
}
Future<bool> update(Todo _todo) async {
http.Response response = await http.put('${URL}', body: {
'id': _todo.id.toString(),
'description': _todo.description,
'done': _todo.done.toString()
});
return (response.statusCode == 200 || response.statusCode == 201);
}
}

Muito bem, ja estamos quase chegando no final deste artigo, vamos agora configurar nosso controller que iremos utilizar o mobx, atenção o mobx utiliza uma classe abstract e devemos executar o build_runner (instalado nas dev dependency) para gerar a implementação da abstração.

import 'package:flutter_todo/todo/todo.model.dart';
import 'package:flutter_todo/todo/todo.service.dart';
import 'package:mobx/mobx.dart';
part 'todo.controller.g.dart';class TodoController = _TodoControllerBase with _$TodoController;abstract class _TodoControllerBase with Store {
@observable
List<Todo> todos = [];
@action
fetchTodos() async {
todos = await TodoService().listAll();
}
@action
create(String description) async {
await TodoService().create(description);
fetchTodos();
}
@action
remove(int id) async {
await TodoService().remove(id);
fetchTodos();
}
@action
update(Todo todo) async {
await TodoService().update(todo);
fetchTodos();
}
}

Devemos agora rodar o builder para criar a part “todo.controller.g.dart”: flutter packages pub run build_runner build

Feito isto nossa ultima eta sera criar nossa page, ela sera a interface que o usuário manipulara no mobile.

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_todo/todo/todo.controller.dart';
import 'package:flutter_todo/todo/todo.model.dart';
class TodoPage extends StatefulWidget {
@override
_TodoPageState createState() => _TodoPageState();
}
class _TodoPageState extends State<TodoPage> {
TodoController _controller = new TodoController();
TextEditingController _textEditingController = new TextEditingController();
@override
void initState() {
super.initState();
_controller.fetchTodos();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todo APP'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
_controller.fetchTodos();
},
)
],
),
body: _body(),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.white,
child: Icon(Icons.add),
onPressed: () => _modelAddTodo(context),
),
);
}
Widget _body() {
return Padding(
padding: EdgeInsets.all(16),
child: Observer(builder: (_) {
return ListView.builder(
itemCount: _controller.todos?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
Todo _todo = _controller.todos[index];
return ListTile(
title: Text(_todo.description),
leading: Checkbox(
activeColor: Colors.white,
checkColor: Colors.black,
value: _todo.done,
onChanged: (val) => {_todo.done = val, _updateTodo(_todo)},
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
_controller.remove(_todo.id);
},
),
onLongPress: () {
_textEditingController.text = _todo.description;
_modelAddTodo(context, todo: _todo);
},
);
},
);
}),
);
}
_modelAddTodo(context, {Todo todo}) {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(todo != null ? 'Update todo' : 'Create todo'),
content: TextFormField(
controller: _textEditingController,
),
actions: <Widget>[
FlatButton(
onPressed: () {
todo != null ? _updateTodo(todo) : _addTodo();
},
child: Text('Save'),
)
],
);
},
).then((_) => {_textEditingController.clear()});
}
_addTodo() async {
if (_validInput()) {
await _controller.create(_textEditingController.text);
Navigator.pop(context);
}
}
_updateTodo(Todo todo) async {
if (_validInput()) {
todo.description = _textEditingController.text;
await _controller.update(todo);
Navigator.pop(context);
return;
}
await _controller.update(todo);
}
bool _validInput() {
return (_textEditingController.text != null &&
_textEditingController.text != '');
}
}

No final com seu emulador de preferencia aberto, executamos no terminal o comando para rodar o projeto: flutter run

devemos ter algo como:

Muito bem chegamos ao final deste longo exemplo desta bela stack, me conta a baixo o que acharam, lembre-se que nem todos os melhores padrões de projetos foram seguidos, alguns conceitos foram deixados de forma para simplificar o entendimento, quaisquer duvidas entre em contato comigo pelo Telegram ou Linkedln.

--

--

Deivid Willyan
Nerdzão/Nerdgirlz

Fullstack developer | Java | Flutter | NodeJs | Angular | Python