DEV Community

Marcus Brasizza
Marcus Brasizza

Posted on

12 1

Criação de uma API em Shelf

Fala pessoal!

Vamos ao primeiro artigo da série que se tudo der certo sai 100%

Já aviso que o artigo é grande mas é necessário para que seja colocado exatamente o passo a passo de como fazer!

O json para testar no postman se encontra aqui:

POSTMAN

Vamos iniciar explicando como será nossa api:
Iremos conectar em um banco mysql onde terá nosso banco de dados chamado de delivery e nele terá uma tabela chamada orders, e nela estarão todos nossos pedidos por provider (ifood/rappi) e os status (Iniciado, Em andamento, Finalizado, Em Rota e Completo) onde

  • Iniciado: O servidor recebeu o pedido naquele momento
  • Em andamento: Alguém iniciou a produção desse pedido
  • Finalizado: A produção do pedido foi finalizado
  • Em rota: O pedido saiu do restaurante e está indo para o cliente
  • Finalizado: Pedido foi entregue

Teremos as rotas de pegar Todos os pedidos, pegar um pedido por ID, Inserir, Alterar e excluir o pedido por ID

Nossa tabela do mysql

CREATE TABLE `orders` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(200) DEFAULT NULL,
`address` text DEFAULT NULL,
`order_id` varchar(45) DEFAULT NULL,
`order_total` decimal(20,2) DEFAULT NULL,
`provider` varchar(45) DEFAULT NULL,
`order_provider_id` varchar(255) DEFAULT NULL,
`status` tinyint(1) DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`updated_at` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
view raw order.sql hosted with ❤ by GitHub

INICIANDO O SHELF

O Dart nos traz uma facilidade absurda na criação dos nossos projetos, que são os templates. Com eles você pode já criar seu projeto de um jeito fácil e já é incluído algumas coisas automaticamente nele.

Vamos criar nosso projeto com o template do shelf mesmo assim

dart create -t server-shel api
Com isso ele já vai jogar nas depenências o shelf e o shelf_router e um arquivo teste rest no seu bin/server.dart

podemos executar ele clicando ou no debug ou no run do server.dart e abrir o link http://0.0.0.0:8080/, você vai ver que ele vai aparecer um 'Hello World!' , sinal de que está tudo funcionando

DEPENDENCIAS

Tentarei usar o mínimo de dependências possíveis no server, então iremos usar o get_it para gerenciar nossas dependências, um package de mysql que eu optei nesse projeto usar o mysql_client, pois está atualizado há pouco tempo e diz no package o suporte pro mysql8 e mariadb 10 e sabemos que o mysql1 está meio chatinho de usar então dei uma chance pra ele! e pra finalizar o dotenv, que iremos utilizar para pegar alguns dados para iniciar nosso servidor shelf, como porta e dados do mysql, mesmo sabendo que não é o local mais seguro do mundo, como estará no server-side não vejo muito problema inicialmente.
Para adicionar rapidamente no seu projeto todos esses packages faremos de uma vez só

dart pub add get_it mysql_client dotenv

ARQUIVOS UTEIS

Iremos criar um concentrador de logs que chamaremos de
developer e iremos colocar em lib/src/core/developer
iremos criar o developer.dart

import 'dart:developer';
class Developer {
static void logInstance(dynamic instance) {
log('Start the ${instance.runtimeType} instance');
}
static void logError({required String errorText, required Object error, String? errorName, StackTrace? stackTrace, DateTime? time}) {
log(errorText, error: error, stackTrace: stackTrace, time: time ?? DateTime.now(), name: errorName ?? '');
}
Developer._();
}
view raw developer.dart hosted with ❤ by GitHub

Estou também fazendo uns testes de abstração de banco de dados, portanto é meramente experimental, onde teoricamente eu poderia usar qualquer banco de dados (que podemos testar depois, por exemplo o hive), existem alguns métodos nele que eu ainda não implementei, pois como foi construído para o hive, consegui adaptar muito bem para o mysql

Vamos criar uma pasta em lib/src/core/database e colocaremos nosso database.dart e o nosso mysql_database.dart

abstract class Database {
Future<T?> openDatabase<T>(Map<String, dynamic> path);
Future<void> closeDatabase();
Future<List<T>?> getData<T>(String query);
Future<T?> getUnique<T>(String query);
Future<int> insert<T>({required String tableName, required T value});
Future<int> insertAll<T>({required String tableName, required List<T> value, bool clear = false});
Future<bool> update<T>({required String tableName, required T value});
Future<bool> delete<T>({required String tableName, required T value});
Future<void> clear<T>(String s);
Future<T?> objectExists<T>({required T object, required String tableName});
Future<int?> objectIndex<T>({required T object, required String tableName});
}
view raw database.dart hosted with ❤ by GitHub
import 'package:api/src/core/developer/developer.dart';
import 'package:mysql_client/mysql_client.dart';
import './database.dart';
class MysqlDatabase implements Database {
MySQLConnection? conn;
MysqlDatabase._();
static MysqlDatabase? _instance;
static MysqlDatabase get i {
_instance ??= MysqlDatabase._();
return _instance!;
}
@override
Future<void> clear<T>(String s) {
throw UnimplementedError();
}
@override
Future<int> insertAll<T>({required String tableName, required List<T> value, bool clear = false}) {
throw UnimplementedError();
}
@override
Future<T?> objectExists<T>({required T object, required String tableName}) {
throw UnimplementedError();
}
@override
Future<int?> objectIndex<T>({required T object, required String tableName}) {
throw UnimplementedError();
}
@override
Future<void> closeDatabase() async {
await conn?.close();
}
@override
delete<T>({required String tableName, required T value}) async {
final key = (value as Map).keys.first;
final val = (value.values.first);
final result = await conn?.execute('Delete from $tableName where $key = $val');
if (result != null) {
if (result.affectedRows.toInt() > 0) {
return true;
} else {
return false;
}
}
return false;
}
@override
Future<List<T>?> getData<T>(String query) async {
final listResult = <T>[];
final result = await conn?.execute(query);
if (result != null) {
for (final row in result.rows) {
listResult.add(row.assoc() as T);
}
return listResult;
}
return null;
}
@override
Future<T?> getUnique<T>(String query) async {
final result = await conn?.execute(query);
if (result == null) {
return null;
}
if (result.rows.isEmpty) {
return null;
}
return (result.rows.first.assoc()) as T;
}
@override
Future<int> insert<T>({required String tableName, required T value}) async {
try {
final tableFields = ((value as Map).keys).join(',');
final tableWillCards = (value).keys.map((e) => ":$e").join(',');
final res = await conn?.execute(
"INSERT INTO $tableName ($tableFields) VALUES ($tableWillCards)",
value as Map<String, dynamic>?,
);
if (res == null) {
return 0;
}
if (res.affectedRows.toInt() == 1) {
return res.lastInsertID.toInt();
}
return 0;
} catch (e, s) {
Developer.logError(errorText: 'Fail to save', error: e, stackTrace: s, errorName: runtimeType.toString());
return 0;
}
}
@override
Future<MysqlDatabase?> openDatabase<MysqlDatabase>(Map<String, dynamic> path) async {
conn = await MySQLConnection.createConnection(
host: path['host'],
port: int.parse(path['port']),
userName: path['userName'],
password: path['password'],
databaseName: path['databaseName'],
secure: int.parse(path['secure']) == 0 ? false : true,
);
conn?.connect();
Developer.logInstance(this);
return this as MysqlDatabase;
}
@override
Future<bool> update<T>({required String tableName, required T value}) async {
try {
final tableFields = ((value as Map).keys).toList();
final tableWillCards = (value).keys.map((e) => ":$e").toList();
final List<String> queryFields = [];
for (var i = 0; i < tableFields.length; i++) {
queryFields.add("${tableFields[i]} = ${tableWillCards[i]}");
}
final String fields = (queryFields.join(','));
final res = await conn?.execute(
"UPDATE $tableName SET $fields where id = ${value['id']}",
value as Map<String, dynamic>?,
);
if (res == null) {
return false;
}
if (res.affectedRows.toInt() == 1) {
return true;
}
return false;
} catch (e, s) {
Developer.logError(errorText: 'Fail to save', error: e, stackTrace: s, errorName: runtimeType.toString());
return false;
}
}
}

ESTRUTURA DO PROJETO

Iremos utilizar uma estrutura de

  • Controller
    • Service
    • Repository

Nesse projeto escolhi utilizar o service layer para que possamos ter mais liberdade para o repository, por exemplo quando atualizamos um pedido ou inserimos, a responsabilidade do repository é só inserir ou alterar, deixamos o service responsável por retornar o objeto novo ou alterado, assim dividimos as responsabilidades e deixamos o nosso repository somente para fazer a conexão entre o nosso banco e nada mais!

Para ficar mais fácil , fora da pasta bin, iremos criar uma pasta lib, e dentro dela a src onde iremos colocar nosso código isolado dos demais

MODEL ORDER e ORDER STATUS

Primeiramente precisamos criar nosso model, e olhando a nossa tabela temos os campos que serão representados da nossa tabela.Podemos notar que os campos estão em snake_case e por padrão o flutter 'sugere' que as propriedades sejam em camelCase, então teremos que fazer algumas traduções de campos além de para ficar mais fácil iremos criar um enum para o nosso campo de status

O enum do status que iremos criar na pasta lib/src/enum/order_status.dart

enum OrderStatus {
iniciado(label: "Iniciado"),
andamento(label: 'Em andamento'),
finalizado(label: 'Finalizado'),
emRota(label: 'Em Rota'),
completo(label: 'Completo');
final String label;
const OrderStatus({required this.label});
}

O nosso model do Order, criei com a extensão do Dart class generator , ele ajuda demais a construir rapidamente um model

Model que iremos criar na pasta lib/src/data/model/order.dart

import 'dart:convert';
import 'package:api/src/enum/order_status.dart';
class Order {
final int? id;
final String name;
final String address;
final String orderId;
final double orderTotal;
final String provider;
final String orderProviderId;
final OrderStatus status;
final DateTime createdAt;
final DateTime updatedAt;
Order({
this.id,
required this.name,
required this.address,
required this.orderId,
required this.orderTotal,
required this.provider,
required this.orderProviderId,
required this.status,
required this.createdAt,
required this.updatedAt,
});
Order copyWith({
int? id,
String? name,
String? address,
String? orderId,
double? orderTotal,
String? provider,
String? orderProviderId,
OrderStatus? status,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Order(
id: id ?? this.id,
name: name ?? this.name,
address: address ?? this.address,
orderId: orderId ?? this.orderId,
orderTotal: orderTotal ?? this.orderTotal,
provider: provider ?? this.provider,
orderProviderId: orderProviderId ?? this.orderProviderId,
status: status ?? this.status,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'id': id,
'name': name,
'address': address,
'orderId': orderId,
'orderTotal': orderTotal,
'provider': provider,
'orderProviderId': orderProviderId,
'status': status.index,
'createdAt': createdAt.millisecondsSinceEpoch,
'updatedAt': updatedAt.millisecondsSinceEpoch,
};
}
Map<String, dynamic> toDatabase() {
return <String, dynamic>{
'id': id,
'name': name,
'address': address,
'order_id': orderId,
'order_total': orderTotal,
'provider': provider,
'order_provider_id': orderProviderId,
'status': status.index,
'created_at': createdAt,
'updated_at': updatedAt,
};
}
factory Order.fromMap(Map<String, dynamic> map) {
return Order(
id: (map['id'] == null) ? null : int.parse(map['id'].toString()),
name: map['name'] as String,
address: map['address'] as String,
orderId: map['order_id'] as String,
orderTotal: double.parse(map['order_total'].toString()),
provider: map['provider'] as String,
orderProviderId: map['order_provider_id'] as String,
status: OrderStatus.values.firstWhere((orderStatus) => orderStatus.index == int.parse(map['status'].toString()), orElse: () => OrderStatus.iniciado),
createdAt: (map['created_at'] == null) ? DateTime.now() : DateTime.parse(map['created_at']),
updatedAt: (map['updated_at'] == null) ? DateTime.now() : DateTime.parse(map['updated_at']),
);
}
String toJson() => json.encode(toMap());
factory Order.fromJson(String source) => Order.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() {
return 'Order(id: $id, name: $name, address: $address, orderId: $orderId, orderTotal: $orderTotal, provider: $provider, orderProviderId: $orderProviderId, status: $status, createdAt: $createdAt, updatedAt: $updatedAt)';
}
@override
bool operator ==(covariant Order other) {
if (identical(this, other)) return true;
return other.id == id && other.name == name && other.address == address && other.orderId == orderId && other.orderTotal == orderTotal && other.provider == provider && other.orderProviderId == orderProviderId && other.status == status && other.createdAt == createdAt && other.updatedAt == updatedAt;
}
@override
int get hashCode {
return id.hashCode ^ name.hashCode ^ address.hashCode ^ orderId.hashCode ^ orderTotal.hashCode ^ provider.hashCode ^ orderProviderId.hashCode ^ status.hashCode ^ createdAt.hashCode ^ updatedAt.hashCode;
}
Order updateMap(Map<String, dynamic> data) {
return Order(
id: id,
name: data['name'] ?? name,
address: data['address'] ?? address,
orderId: data['order_id'] ?? orderId,
orderTotal: data['order_total'] ?? orderTotal,
provider: data['provider'] ?? provider,
orderProviderId: data['order_provider_id'] ?? orderProviderId,
status: OrderStatus.values.firstWhere((orderStatus) => orderStatus.index == (int.tryParse(data['status'].toString()) ?? status.index), orElse: () => OrderStatus.iniciado),
createdAt: data['created_at'] ?? createdAt,
updatedAt: data['updated_at'] ?? updatedAt,
);
}
}
view raw order.dart hosted with ❤ by GitHub

Pontos importantes da nossa model que foram customizados, como por exemplo o orderId que no nosso banco é order_id , e os outros, que tiveram que ser traduzidos no fromMap exatamente como está na tabela

Outro ponto importante foi a criação do toDatabase e do updateMap, que no decorrer vou explicar o motivo da criação

Módulo order

Para separar nosso projeto , iremos criar uma pasta order em
lib/src/modules/order e criaremos 2 arquivos
o order_controller.dart e order_route.dart

A order_controller será nosso controlador de todas as ações que o nosso endpoint precisará fazer, e o order_route será onde iremos especificar a nossas rotas e o que efetivamente faremos nelas

Na nossa controller teremos o:

  • getAll , pra pegar todos os pedidos
  • getById, para pegar um pedido específico
  • save, para inserir o pedido no banco de dados
  • update, para atualizar um pedido específico
  • delete, para deletar um pedido específico

Ta, mas como faremos isso?
Como eu havia mencionado, iremos utilizar 2 camadas acima da controller: a service e a repository.

Iremos inicialmente criar a abstração da service e depois implementar. Vamos chamar de order_service e vamos colocar ela dentro da pasta lib/src/service/order_service.dart

abstract class OrderService {
Future<List<Order>?> getAll();
Future<Order?> getById(int id);
Future<Order?> save(Order order);
Future<Order?> delete(int id);
Future<Order?> update(int id, {required Map<String, dynamic> data});
}

onde:
  • getAll pode ou não retornar uma lista de Order,

  • getById, pode ou não retornar um Order passando um id,

  • delete, deleta da sua base de dados com base em um id e te retorna o Order deletado

  • save, salva os dados enviados para o endpoint , retornando o Order salvo

  • update , atualiza um Order com base em um id, atualizando os campos que foram preenchidos para atualização

import 'package:api/src/data/model/order.dart';
import 'package:api/src/data/repository/order_repository.dart';
import './order_service.dart';
class OrderServiceImpl implements OrderService {
final OrderRepository _repository;
OrderServiceImpl({
required OrderRepository repository,
}) : _repository = repository;
@override
Future<Order?> delete(int id) async {
final order = await getById(id);
if (order == null) {
return null;
}
final deleted = await _repository.delete(id);
if (!deleted) {
return null;
}
return order;
}
@override
Future<List<Order>?> getAll() async {
return _repository.getAll();
}
@override
Future<Order?> getById(int id) async {
return await _repository.getById(id);
}
@override
Future<Order?> save(Order order) async {
final saved = await _repository.save(order);
if (saved == null) {
return null;
}
return getById(saved);
}
@override
Future<Order?> update(int id, {required Map<String, dynamic> data}) async {
final order = await getById(id);
if (order == null) {
return null;
}
final newOrder = order.updateMap(data).copyWith(updatedAt: DateTime.now());
final updated = await _repository.update(id, newOrder);
if (!updated) {
return null;
}
return getById(id);
}
}

Se olharmos no service, iremos ver que temos algumas particularidades para deixar nosso código mais limpo, como por exemplo as checagens para deletar ou atualizar um registro, além de por exemplo ele retornar um objeto Order para sua service seja com ele atualizado no caso do update ou o que foi inserido naquele momento, e no caso do delete em particular, ele te manda o registro que foi excluído!

Além disso iremos utilizar o método updateMap que, como pegamos o objeto antes de fazer o update, nós atualizamos o mesmo objeto com os dados vindos requisição, assim nós garantimos que só irão ser alterados os campos que forem enviados pela requisião de UPDATE, além de atualizar o campo de data de atualização e ai sim enviamos para o banco de dados para atualização

Também iremos criar um repository chamado order_repository na pasta
lib/src/data/repository/order_repository.dart

import 'package:api/src/data/model/order.dart';
abstract class OrderRepository {
Future<List<Order>?> getAll();
Future<Order?> getById(int id);
Future<int?> save(Order order);
Future<bool> delete(int id);
Future<bool> update(int id, Order order);
}

A grande diferença entre o repository e o service estão nos métodos de inserir, deletar e atualizar, onde o repository só precisa entregar pro service, se deu certo ou não, e só no caso do inserir que ele precisa retornar o id inserido para que o service possa tomar as providencias necessárias.

A implementação do repository para explicação

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:api/src/core/database/database.dart';
import 'package:api/src/data/model/order.dart';
import './order_repository.dart';
class OrderRepoistoryImpl implements OrderRepository {
final Database _database;
OrderRepoistoryImpl({
required Database database,
}) : _database = database;
@override
Future<List<Order>?> getAll() async {
final data = await _database.getData("Select * from orders");
if (data != null) {
return data.map<Order>((o) => Order.fromMap(o)).toList();
}
return null;
}
@override
Future<Order?> getById(int id) async {
final data = await _database.getUnique("Select * from orders where id = $id");
if (data != null) {
return Order.fromMap(data);
}
return null;
}
@override
Future<int?> save(Order order) async {
final data = await _database.insert(
tableName: 'orders',
value: order.toDatabase(),
);
if (data != 0) {
return data;
}
return null;
}
@override
Future<bool> delete(int id) async {
final deleted = await _database.delete(
tableName: 'orders',
value: {'id': id},
);
return deleted;
}
@override
Future<bool> update(int id, Order order) async {
final updated = await _database.update(tableName: 'orders', value: order.toDatabase());
return updated;
}
}

No repository fazemos a conexão com o banco de dados que por fim faz todas as ações necessárias, além de no salvar, nós utilizamos o método toDatabase que criamos que basicamente é para normalizar os nomes dos campos com os nomes das tabelas do banco de dados, assim facilita nosso trabalho de ter que normalizar (ou seja, colocar os mesmos nomes da tabela)

Com tudo isso criado, podemos finalmente criar nossa controller.

A controller também terá os seguintes métodos iniciais

  • getAll
  • getById
  • save
  • update
  • delete

A diferença dela para as camadas acima é que ela vai ser nossa ligação entre o shelf e o service que por sua vez se comunica com o repository

Como você deve ter notado, normalmente fazemos a inversão dependência e injetamos por exemplo o repository na service e o database no repository e por sua vez injetamos o service na controller, assim podemos fazer um código o mínimo de agregação fixa e se caso for preciso mudar a instância, mudamos somente na injeção na classe.

Uma coisa importante no nosso controller é que como estamos trabalhando com o shelf, o retorno de todos os métodos devem ser um Response, para que o shelf possa responder corretamente com 200, ou 400 ou 500 para o nosso cliente na ponta onde está acessando o endpoint específico que iremos criar logo em seguida.

import 'dart:convert';
import 'package:api/src/core/developer/developer.dart';
import 'package:api/src/data/model/order.dart';
import 'package:api/src/service/order_service.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
class OrderController {
final OrderService _service;
OrderController({
required OrderService service,
}) : _service = service;
Future<Response> getAll(Request request) async {
final orders = await _service.getAll();
final list = [];
if (orders != null) {
for (var order in orders) {
list.add(order.toMap());
}
var body = json.encode(list);
return Response.ok(body, headers: {'Content-Type': 'text/json'});
}
return Response.notFound('Orders not found', headers: {'Content-Type': 'text/json'});
}
Future<Response> getById(Request request) async {
try {
final id = int.parse(request.params['id'].toString());
final order = await _service.getById(id);
if (order != null) {
return Response.ok(order.toJson(), headers: {'Content-Type': 'text/json'});
} else {
return Response.notFound('Order $id not found', headers: {'Content-Type': 'text/json'});
}
} catch (e, s) {
Developer.logError(errorText: 'Error to process', error: e, stackTrace: s, errorName: runtimeType.toString());
return Response.internalServerError(body: "id not found");
}
}
Future<Response> save(Request request) async {
final data = await request.readAsString();
final order = Order.fromJson(data);
final orderSaved = await _service.save(order);
if (orderSaved == null) {
return Response.internalServerError(body: "Error to save");
}
return Response.ok(orderSaved.toJson(), headers: {'Content-Type': 'text/json'});
}
Future<Response> delete(Request request) async {
final id = int.parse(request.params['id'].toString());
final orderDeleted = await _service.delete(id);
if (orderDeleted == null) {
return Response.internalServerError(body: "Error to delete");
}
return Response.ok(orderDeleted.toJson(), headers: {'Content-Type': 'text/json'});
}
Future<Response> update(Request request) async {
final body = await (request.readAsString());
final id = int.parse(request.params['id'].toString());
final order = await _service.update(id, data: json.decode(body));
return Response.ok(order?.toJson());
}
}

O controller que tem o service injetado será nossa ponta para ligar diretamente no shelf, podemos ver que na maioria dos casos, faz uma logica simples e chama o service que faz toda a lógica juntamente com o controller retornando somente o objeto ou um nulo indicando erro, sempre respeitando o máximo possível das respostas http:

  • 2xx para OK
  • 4xx para dados com problemas (mas processou)
  • 5xx erro crítico no nosso servidor ou banco de dados

Eu particularmente gosto de nomear minhas instâncias em constantes para ficar mais fácil a recuperação, então como sabemos que teremos que injetar o mysql, o nosso service do order e o repository, já vamos criar a nossa classe que ficará com os nomes das constantes.

class Consts {
Consts._();
static final String mysqlInstance = '/database/mysq/instance';
static final String orderRepository = '/class/order/instance/repository';
static final String orderService = '/class/order/instance/service';
}
view raw consts.dart hosted with ❤ by GitHub

Eu coloco nomes grandes, porque como esta na propriedade não tem problema nenhum porque vai pegar de Consts.mysqlInstance por exemplo

Até agora não colocamos um dedo no código do shelf propriamente dito e provavelmente se criou um template do shelf igual descrito acima o seu server.dart vai estar parecido com algo assim

import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';
final _router = Router()
..get('/', _rootHandler)
..get('/echo/<message>', _echoHandler);
Response _rootHandler(Request req) {
return Response.ok('Hello, World!\n');
}
Response _echoHandler(Request request) {
final message = request.params['message'];
return Response.ok('$message\n');
}
void main(List<String> args) async {
// Use any available host or container IP (usually `0.0.0.0`).
final ip = InternetAddress.anyIPv4;
// Configure a pipeline that logs requests.
final handler = Pipeline().addMiddleware(logRequests()).addHandler(_router);
// For running in containers, we respect the PORT environment variable.
final port = int.parse(Platform.environment['PORT'] ?? '8080');
final server = await serve(handler, ip, port);
print('Server listening on port ${server.port}');
}

isso quer dizer que , se for executado esse shelf do jeito que está e entrar no seu ip:8080 ele vai mostrar na tela um Hello world

A primeira coisa que vamos fazer ai é criar um arquivo .env na raiz do seu projeto para que possamos colocar os dados do seu banco de dados

server_port=9080
host=127.0.0.1
port=3306
userName=root
password=mypasswordRoot
databaseName=delivery
secure=0
view raw .env hosted with ❤ by GitHub

Vamos usar o dotenv para carregar esse arquivo e também vamos usar o GetIt para guardar essa instância para um futuro uso. Além disso vamos no server.dart mesmo fazer a conexão como o mysql e também guardar essa instância.

void main(List<String> args) async {

final Env env = Env.i..load();
  GetIt.I.registerSingleton(env);

  final MysqlDatabase mysql = await MysqlDatabase.i.openDatabase(
    {
      'host': env['host'] ?? '',
      'port': env['port'] ?? '',
      'userName': env['userName'] ?? '',
      'password': env['password'] ?? '',
      'databaseName': env['databaseName'] ?? '',
      'secure': env['secure'] ?? '',
    },
  );
  GetIt.I.registerSingleton<Database>(mysql, instanceName: Consts.mysqlInstance);
....
}
Enter fullscreen mode Exit fullscreen mode

Com isso teremos nossa conexão com o mysql sempre disponível no getIt.

Criando as rotas no shelf

Criamos um order_route.dart que ainda está vazio e vamos criar nossas rotas nesse arquivo para deixar bem separado.

Como já criamos nosso controller, o nosso service E a nossa repository iremos iniciar a instância deles somente neste ponto , para que a responsabilidade da criação das instâncias seja somente onde ela é chamada de fato!
Iremos criar um método estático chamado routes onde iremos criar nossas chamadas de rotas como descritas abaixo

import 'package:api/src/core/consts.dart';
import 'package:api/src/core/database/database.dart';
import 'package:api/src/data/repository/order_repository.dart';
import 'package:api/src/data/repository/order_repository_impl.dart';
import 'package:api/src/modules/order/order_controller.dart';
import 'package:api/src/service/order_service.dart';
import 'package:api/src/service/order_service_impl.dart';
import 'package:get_it/get_it.dart';
import 'package:shelf_router/shelf_router.dart';
class OrderRoute {
OrderRoute._();
static Router routes(Router router) {
GetIt.I.registerSingleton<OrderRepository>(
OrderRepositoryImpl(database: GetIt.I.get<Database>(instanceName: Consts.mysqlInstance)),
instanceName: Consts.orderRepository,
);
GetIt.I.registerSingleton<OrderService>(
OrderServiceImpl(repository: GetIt.I.get<OrderRepository>(instanceName: Consts.orderRepository)),
instanceName: Consts.orderService,
);
final orderController = OrderController(service: GetIt.I.get<OrderService>(instanceName: Consts.orderService));
router.add('get', '/orders', orderController.getAll);
router.add('get', '/order/<id>', orderController.getById);
router.add('post', '/order', orderController.save);
router.add('put', '/order/<id>', orderController.update);
router.add('delete', '/order/<id>', orderController.delete);
return router;
}
}

podemos ver que foram criados 5 rotas bases

  • a /orders que é o nosso getAll
  • a /order/id que é a nossa rota de pegar um order por id
  • o /order que vai salvar o nosso objeto de order
  • o /order/id que vai atualizar a nossa order com base em um id
  • a /order/id que vai deletar o nosso registro com base na ID

Tudo isso mandando os verbos certos, get onde é requisição de informação, post e put e delete quando é envio de informação para o servidor

Feito isso iremos agora colocar essa rota no nosso server.dart
Colocaremos a porta do shelf padrão como vindo do .env, e se não encontrar ele assume a porta 8080

Iremos fazer de um jeito inicialmente que só será possível incluir as rotas do order, mas caso nós formos progredindo podemos alterar para contemplar mais rotas

import 'dart:io';
import 'package:api/src/core/config.dart';
import 'package:api/src/core/consts.dart';
import 'package:api/src/core/database/database.dart';
import 'package:api/src/core/database/mysql_database.dart';
import 'package:api/src/modules/order/order_route.dart';
import 'package:get_it/get_it.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';
void main(List<String> args) async {
final Env env = Env.i..load();
GetIt.I.registerSingleton(env);
final MysqlDatabase mysql = await MysqlDatabase.i.openDatabase(
{
'host': env['host'] ?? '',
'port': env['port'] ?? '',
'userName': env['userName'] ?? '',
'password': env['password'] ?? '',
'databaseName': env['databaseName'] ?? '',
'secure': env['secure'] ?? '',
},
);
GetIt.I.registerSingleton<Database>(mysql, instanceName: Consts.mysqlInstance);
final ip = InternetAddress.anyIPv4;
final Router router = Router();
final handler = Pipeline().addMiddleware(logRequests()).addHandler(OrderRoute.routes(
router,
));
final port = int.parse(env['server_port'] ?? '8080');
final server = await serve(handler, ip, port);
print('Server listening on port ${server.port}');
}
view raw server.dart hosted with ❤ by GitHub

e com isso terminamos nossa primeira parte do servidor shelf. Sei que é muita informação e muito texto, mas se você se perdeu em algum lugar, você inicialmente pode tentar me chamar no discord pois me ajuda demais a entender onde está o problema e ajudar a corrigir!

Ou olhar no repositório GIT

Espero que tenham gostado e iremos fazer nosso primeiro app utilizando esse backend no próximo artigo!

Top comments (0)

Billboard image

📊 A side-by-side product comparison between Sentry and Crashlytics

A free guide pointing out the differences between Sentry and Crashlytics, that’s it. See which is best for your mobile crash reporting needs.

See Comparison

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay