Projeto cafe fullstack Django e Vue.js

Criação de projeto de cafe fullstack Django, Vue.js, Postman e Bootstrap
Voltar

Material complementar:


Conceito:

Criar sistema web RESTful fullstack, com backend Django REST Framework - DRF (viewsets e routers) e frontend Vue.js TypeScript (vue router), com funções de CRUD (criar, listar, ver específico, editar e excluir) de cafés Parisienses. Testes através do Postman, estilização frontend através do Bootstrap (Bootstrap vue next). Possui os projetos "paris_cafe" (backend Django REST Framework) e "cafe_frontend" (frontend Vue.js TypeScript) integrados como sistema RESTful fullstack.


Backend (Django REST Framework):

  1. Criar projeto DRF:
    
    pip3 install django djangorestframework markdown django-filter django-cors-headers pytest pytest-django pytest-html model_bakery --break-system-packages
    django-admin startproject paris_cafe
    cd paris_cafe
    python manage.py startapp cafes
    
  2. Em paris_cafe/settings.py, informar conteúdo faltante:
    
    INSTALLED_APPS = [
        # Outro conteúdo
        'rest_framework',
        'corsheaders',
        'cafes',
    ]
    
    MIDDLEWARE = [
        # Outro conteúdo
        'corsheaders.middleware.CorsMiddleware',
    ]
    
    REST_FRAMEWORK = {
        'DEFAULT_RENDERER_CLASSES': [
            'rest_framework.renderers.JSONRenderer',
        ],
        'DEFAULT_PARSER_CLASSES': [
            'rest_framework.parsers.JSONParser',
        ]
    }
    
    CORS_ALLOW_ALL_ORIGINS = True
    # CORS_ALLOWED_ORIGINS = [
    #    "http://localhost:8080",
    # ]
    
  3. Em cafes/models.py, criar Model Cafeteria:
    
    from django.db import models
    from django.core.validators import MinValueValidator, MaxValueValidator
    
    class Cafeteria(models.Model):
        nome = models.CharField(max_length=255)
        data_inauguracao = models.DateField()
        endereco = models.CharField(max_length=255)
        avaliacao = models.FloatField(validators=[MinValueValidator(0.0), MaxValueValidator(10.0)])
    
        def __str__(self):
            return f"{self.nome} ({self.avaliacao}/10)"
    
  4. Em cafes/serializers.py, criar arquivo de serializadores:
    
    from rest_framework import serializers
    from .models import Cafeteria
    
    class CafeteriaSerializer(serializers.ModelSerializer):
        class Meta:
            model = Cafeteria
            fields = '__all__'
    
  5. Em cafes/views.py, criar viewsets:
    
    from rest_framework import viewsets
    from .models import Cafeteria
    from .serializers import CafeteriaSerializer
    
    class CafeteriaViewSet(viewsets.ModelViewSet):
        queryset = Cafeteria.objects.all()
        serializer_class = CafeteriaSerializer
    
  6. Em cafes/urls.py, criar arquivo de routes locais:
    
    from django.urls import path, include
    from rest_framework.routers import DefaultRouter
    from .views import CafeteriaViewSet
    
    router = DefaultRouter()
    router.register(r'cafes', CafeteriaViewSet)
    
    urlpatterns = [
        path('', include(router.urls)),
    ]
    
  7. Em paris_cafe/urls.py, editar arquivo de routes globais:
    
    from django.contrib import admin
    from django.urls import path, include
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('', include('cafes.urls')),
    ]
    
  8. Executar projeto:
    
    python manage.py makemigrations
    python manage.py migrate
    python manage.py runserver
    
  9. Acessar projeto: http://localhost:8000/cafes/

Testes no Postman:

  1. Listar cafeterias (GET list):
  2. Ver cafeteria 1 (GET detail):
  3. Adicionar cafeteria (POST create):
    • URL: http://localhost:8000/cafes/
    • Método: POST
    • Body (JSON):
      
      {
          "nome": "Les Deux Magots",
          "data_inauguracao": "1884-01-01",
          "endereco": "6 Pl. Saint-Germain des Prés, 75006 Paris, França",
          "avaliacao": 9.8
      }
      
    • Send (enviar)
  4. Editar cafeteria 1 (PUT update):
    • URL: http://127.0.0.1:/cafes/1/
    • Método: PUT
    • Body (JSON):
      
      {
          "nome": "Les Deux Magots ATUALIZADO",
          "data_inauguracao": "1884-01-01",
          "endereco": "6 Pl. Saint-Germain des Prés, 75006 Paris, França",
          "avaliacao": 9.8
      }
                                  
    • Send (enviar)
  5. Excluir cafeteria 1 (DELETE):

Frontend (Vue.js):

  1. Criar projeto Vue.js:
    
    npm install -g @vue/cli
    npm init vue@latest => Nome: cafe-frontend, Escolha: TypeScript, Router (SPA development)
    cd cafe-frontend
    npm install axios bootstrap bootstrap-vue-next --save-dev cypress ts-node @cypress/webpack-dev-server vitest @vitest/ui jsdom @vue/test-utils
    
  2. Em src/components/CafeItem.vue, criar componente item de cafeteria:
    
    <template>
        <b-card class="mb-2">
            <div class="d-flex justify-content-between align-items-center">
                <div>
                    <h5>{{ cafe.nome }}</h5>
                    <p class="mb-0">Avaliação: {{ cafe.avaliacao }}/10</p>
                </div>
                <div>
                    <router-link :to="`/view/${cafe.id}`" class="btn btn-sm btn-primary me-1">Ver</router-link>
                    <router-link :to="`/edit/${cafe.id}`" class="btn btn-sm btn-warning me-1">Editar</router-link>
                    <b-button size="sm" variant="danger" @click="$emit('delete', cafe.id)">Deletar</b-button>
                </div>
            </div>
        </b-card>
    </template>
    
    <script setup lang="ts">
    defineProps<{ cafe: any }>()
    defineEmits(['delete'])
    </script>
    
  3. Em src/router/index.ts, adicionar rotas:
    
    import { createRouter, createWebHistory } from 'vue-router'
    import Home from '../views/Home.vue'
    import AddCafe from '../views/AddCafe.vue'
    import EditCafe from '../views/EditCafe.vue'
    import ViewCafe from '../views/ViewCafe.vue'
    
    const routes = [
        { path: '/', name: 'Home', component: Home },
        { path: '/add', name: 'AddCafe', component: AddCafe },
        { path: '/edit/:id', name: 'EditCafe', component: EditCafe },
        { path: '/view/:id', name: 'ViewCafe', component: ViewCafe }
    ]
    
    export default createRouter({
        history: createWebHistory(),
        routes
    })
    
  4. Em src/services/api.ts, informar URL API backend:
    
    import axios from 'axios'
    
    const api = axios.create({
        baseURL: 'http://127.0.0.1:8000/',
    })
    
    export default api
    
  5. Em src/views/AddCafe.vue, criar tela de cadastro de cafeteria:
    
    <template>
        <div class="container mt-4">
            <h2>Adicionar Cafeteria:</h2>
            <b-form @submit.prevent="addCafe">
                <b-form-input v-model="form.nome" placeholder="Nome" name="nome" class="mb-2" maxlength="255" required />
                <b-form-input v-model="form.data_inauguracao" type="date" name="data_inauguracao" class="mb-2" :max="new Date().toLocaleDateString('sv-SE')" required />
                <b-form-textarea v-model="form.endereco" placeholder="Endereço" name="endereco" class="mb-2" maxlength="255" required />
                <b-form-input v-model.number="form.avaliacao" type="number" step="0.01" min="0" max="10" name="avaliacao" class="mb-2"
                    required />
                <b-button type="submit" variant="success" class="me-2">Confirmar</b-button>
                <router-link to="/" class="btn btn-secondary">Cancelar</router-link>
            </b-form>
        </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue'
    import { useRouter } from 'vue-router'
    import api from '../services/api'
    
    const router = useRouter()
    const form = ref({ nome: '', data_inauguracao: '', endereco: '', avaliacao: 0 })
    
    const addCafe = async () => {
        await api.post('cafes/', form.value)
        router.push('/')
    }
    </script>
    
  6. Em src/views/EditCafe.vue, criar tela edição de cafeteria:
    
    <template>
        <div class="container mt-4">
            <h2>Editar Cafeteria {{ form.id }}:</h2>
            <b-form @submit.prevent="updateCafe">
                <b-form-input v-model="form.nome" name="nome" class="mb-2" maxlength="255" required />
                <b-form-input v-model="form.data_inauguracao" type="date" name="data_inauguracao" class="mb-2" :max="new Date().toLocaleDateString('sv-SE')" required />
                <b-form-textarea v-model="form.endereco" name="endereco" class="mb-2" maxlength="255" required />
                <b-form-input v-model.number="form.avaliacao" type="number" step="0.01" min="0" max="10" name="avaliacao" class="mb-2"
                    required />
                <b-button type="submit" variant="success" class="btn btn-success me-2">Confirmar</b-button>
                <router-link to="/" class="btn btn-secondary">Cancelar</router-link>
            </b-form>
        </div>
    </template>
    
    <script setup lang="ts">
    import { ref, onMounted } from 'vue'
    import { useRoute, useRouter } from 'vue-router'
    import api from '../services/api'
    
    const route = useRoute()
    const router = useRouter()
    const form = ref<any>({})
    
    onMounted(async () => {
        const { data } = await api.get(`cafes/${route.params.id}/`)
        form.value = data
    })
    
    const updateCafe = async () => {
        await api.put(`cafes/${route.params.id}/`, form.value)
        router.push('/')
    }
    </script>
    
  7. Em src/views/Home.vue, criar tela home:
    
    <template>
        <div class="container mt-4">
            <div class="d-flex justify-content-between align-items-center mb-4">
                <h1>Cafés de Paris</h1>
                <div>
                    <router-link to="/add" class="btn btn-success">Adicionar Cafeteria</router-link>
                </div>
            </div>
            <button class="btn btn-sm btn-info me-2 mb-2" @click="toggleOrdenacao">
                {{ ordenado ? 'Ordenação original' : 'Ordenar por avaliação' }}
            </button>
            <CafeItem v-for="cafe in cafes" :key="cafe.id" :cafe="cafe" @delete="deleteCafe" />
        </div>
    </template>
    
    <script setup lang="ts">
    import { ref, onMounted } from 'vue'
    import api from '../services/api'
    import CafeItem from '../components/CafeItem.vue'
    
    const cafes = ref<any[]>([])
    const cafesOriginais = ref<any[]>([])
    const ordenado = ref(false)
    
    const fetchCafes = async () => {
        const { data } = await api.get('cafes/')
        cafesOriginais.value = [...data]
        cafes.value = [...data]
    }
    
    const deleteCafe = async (id: number) => {
        await api.delete(`cafes/${id}/`)
        await fetchCafes()
        if (ordenado.value) {
            ordenarPorAvaliacao()
        }
    }
    
    const ordenarPorAvaliacao = () => {
        cafes.value = [...cafes.value].sort((a, b) => b.avaliacao - a.avaliacao)
    }
    
    const toggleOrdenacao = () => {
        if (!ordenado.value) {
            ordenarPorAvaliacao()
        } else {
            cafes.value = [...cafesOriginais.value]
        }
        ordenado.value = !ordenado.value
    }
    
    onMounted(fetchCafes)
    </script>
    
  8. Em src/views/ViewCafe.vue, criar tela visualização de cafeteria:
    
    <template>
        <div class="container mt-4">
            <h2 class="mb-3">Cafeteria {{ cafe?.id }}:</h2>
            <p><strong>Nome:</strong> {{ cafe?.nome }}</p>
            <p><strong>Inauguração:</strong> {{ cafe?.data_inauguracao.split('-').reverse().join('/') }}</p>
            <p><strong>Endereço:</strong> {{ cafe?.endereco }}</p>
            <p><strong>Avaliação:</strong> {{ cafe?.avaliacao }}/10</p>
            <router-link to="/" class="btn btn-secondary mt-2">Voltar</router-link>
        </div>
    </template>
    
    <script setup lang="ts">
    import { ref, onMounted } from 'vue'
    import { useRoute } from 'vue-router'
    import api from '../services/api'
    
    const cafe = ref<any>(null)
    const route = useRoute()
    
    onMounted(async () => {
        const { data } = await api.get(`cafes/${route.params.id}/`)
        cafe.value = data
    })
    </script>
    
  9. Em src/App.vue, criar index do projeto:
    
    <template>
        <router-view />
    </template>
    
    <script setup lang="ts"></script>
    
    <style></style>
    
  10. Em src/main.ts, configurar dependências:
    
    import { createApp } from "vue";
    import App from './App.vue';
    import router from './router';
    import * as BootstrapVueNext from 'bootstrap-vue-next';
    import 'bootstrap/dist/css/bootstrap.min.css';
    import 'bootstrap-vue-next/dist/bootstrap-vue-next.css';
    
    import {
        BButton,
        BForm,
        BFormInput,
        BFormTextarea,
        BCard
    } from 'bootstrap-vue-next';
    
    const app = createApp(App);
    app.use(router);
    app.component('BButton', BButton);
    app.component('BForm', BForm);
    app.component('BFormInput', BFormInput);
    app.component('BFormTextarea', BFormTextarea);
    app.component('BCard', BCard);
    app.mount('#app');
    
  11. Executar projeto: npm run dev # ou 'npx vite'

Testes backend:

  • Teste unitário: verifica se unidade isolada do código (como função ou método) funciona corretamente;
  • Teste de integração: verifica se diferentes partes do sistema funcionam juntas corretamente;
  • Teste E2E (end-to-end): verifica se sistema funciona como um todo, simulando comportamento ao usuário final.

Testes unitários e de integração, via pytest.

  1. Pasta do projeto backend, criar arquivo pytest.ini:
    
    [pytest]
    DJANGO_SETTINGS_MODULE = paris_cafe.settings
    python_files = tests.py test_*.py *_tests.py
    
  2. Em pasta App 'cafes', criar diretório tests/
  3. Em tests/, criar arquivo conftest.py para uso de fixtures compartilhadas:
    
    import pytest
    from model_bakery import baker
    
    @pytest.fixture
    def cafe():
        return baker.make("cafes.Cafeteria") # 'cafes' é nome da App do projeto
    
  4. Em tests/, criar diretório unit/ para testes unitários
  5. Em tests/unit/, criar arquivo test_models.py para testar models:
    
    import pytest
    from cafes.models import Cafeteria
    
    @pytest.mark.django_db
    def test_cria_cafeteria():
        cafe = Cafeteria.objects.create(
            nome="Café do Centro",
            data_inauguracao="2020-01-01",
            endereco="Rua Principal, 123",
            avaliacao=9.5,
        )
        assert cafe.nome == "Café do Centro"
        assert cafe.avaliacao == 9.5
    
  6. Em tests/unit/, criar arquivo test_serializers.py para testar serializers:
    
    from cafes.serializers import CafeteriaSerializer
    
    def test_cafe_serializer_valid_data():
        data = {
            "nome": "Café Central",
            "data_inauguracao": "2021-01-01",
            "endereco": "Av. Principal, 456",
            "avaliacao": 8.7
        }
        serializer = CafeteriaSerializer(data=data)
        assert serializer.is_valid()
        assert serializer.validated_data["nome"] == "Café Central"
    
  7. Em tests/, criar diretório integration/ para testes de integração
  8. Em tests/integration/, criar arquivo test_views.py para testar views:
    
    import pytest
    from rest_framework.test import APIClient
    from model_bakery import baker
    
    @pytest.mark.django_db
    def test_lista_cafeterias():
        baker.make("cafes.Cafeteria", _quantity=3)
        client = APIClient()
        response = client.get("/cafes/")
        assert response.status_code == 200
        assert len(response.data) == 3
    
    @pytest.mark.django_db
    def test_cria_cafeteria_api():
        client = APIClient()
        data = {
            "nome": "API Café",
            "data_inauguracao": "2023-06-15",
            "endereco": "Avenida API, 789",
            "avaliacao": 7.2,
        }
        response = client.post("/cafes/", data=data, format='json')
        assert response.status_code == 201
        assert response.data["nome"] == "API Café"
    
  9. Executar testes:
    • Via terminal CLI: pytest
    • Via terminal CLI com relatório HTML: pytest --html=relatorio.html
      • Abrir no Browser: start relatorio.html

Testes frontend:

Testes unitários e de integração, via test runner Vitest.

  1. Em src/, criar diretórios tests/unit/ para testes unitários
  2. Modificar vite.config.ts:
    
    import { fileURLToPath, URL } from 'node:url'
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import vueDevTools from 'vite-plugin-vue-devtools'
    
    export default defineConfig({
        plugins: [
            vue(),
            vueDevTools(),
        ],
        resolve: {
            alias: {
                '@': fileURLToPath(new URL('./src', import.meta.url))
            },
        },
        test: {
            globals: true,
            environment: 'jsdom',
            setupFiles: ['./src/tests/setup.ts'],
        },
    })
    
  3. Em package.json, adicionar trecho de script de testes:
    
    "scripts": {
        # Outro conteúdo
        "test": "vitest",
        "test:ui": "vitest --ui",
        "test:e2e": "cypress run --config-file cypress.config.ts --env TS_NODE_COMPILER_OPTIONS='{\"module\":\"ESNext\"}'"
    }
    
  4. Em src/tests/setup.ts, informar configurações de arquivos de testes:
    
    import { beforeAll } from 'vitest'
    
    beforeAll(() => {
        Object.defineProperty(window, 'matchMedia', {
            writable: true,
            value: (query: string) => ({
                matches: false,
                media: query,
                onchange: null,
                addListener: () => { },
                removeListener: () => { },
                addEventListener: () => { },
                removeEventListener: () => { },
                dispatchEvent: () => false,
            }),
        })
    })
    
  5. Editar tsconfig.json:
    
    {
        "files": [],
        "references": [
            {
                "path": "./tsconfig.node.json"
            },
            {
                "path": "./tsconfig.app.json"
            }
        ],
        "compilerOptions": {
            "target": "ES2022",
            "module": "ESNext",
            "moduleResolution": "Bundler",
            "types": [
                "cypress"
            ],
            "esModuleInterop": true,
            "moduleDetection": "force",
            "allowSyntheticDefaultImports": true,
            "resolveJsonModule": true,
            "strict": true
        },
        "include": [
            "cypress/**/*.ts"
        ]
    }
    
  6. Em src/tests/unit/ViewCafe.spec.ts, criar arquivo de testes unitários para tela ViewCafe:
    
    import { mount } from '@vue/test-utils'
    import ViewCafe from '../../views/ViewCafe.vue'
    import { describe, it, expect, vi } from 'vitest'
    import { createMemoryHistory, createRouter } from 'vue-router'
    import api from '@/services/api'
    
    vi.mock('@/services/api')
    
    const mockCafe = {
        id: 1,
        nome: 'Café de Flore',
        data_inauguracao: '1887-01-01',
        endereco: '172 Boulevard Saint-Germain, Paris',
        avaliacao: 9.5,
    }
    
    const router = createRouter({
        history: createMemoryHistory(),
        routes: [{ path: '/view/:id', component: ViewCafe }],
    })
    
    describe('ViewCafe.vue', () => {
        it('exibe detalhes da cafeteria', async () => {
            (api.get as any).mockResolvedValue({ data: mockCafe })
    
            router.push('/view/1')
            await router.isReady()
    
            const wrapper = mount(ViewCafe, {
                global: {
                    plugins: [router],
                },
            })
    
            await new Promise(resolve => setTimeout(resolve, 0))
            expect(wrapper.text()).toContain('Café de Flore')
            expect(wrapper.text()).toContain('1887')
            expect(wrapper.text()).toContain('172 Boulevard Saint-Germain')
            expect(wrapper.text()).toContain('9.5')
        })
    })
    
  7. Em tests/, criar diretório integration/ para testes de integração
  8. Em tests/integration/, criar arquivo ViewCafe.integration.spec.ts para teste de exibição de detalhes da cafeteria:
    
    import { mount } from '@vue/test-utils'
    import ViewCafe from '../../views/ViewCafe.vue'
    import api from '@/services/api'
    import { describe, it, expect, vi } from 'vitest'
    
    const mockCafe = {
        id: 1,
        nome: 'Café Teste',
        data_inauguracao: '2023-07-20',
        endereco: 'Rua Teste, 123',
        avaliacao: 8.5,
    }
    
    vi.mock('@/services/api', () => ({
        default: {
            get: vi.fn(() => Promise.resolve({ data: mockCafe })),
        },
    }))
    
    vi.mock('vue-router', () => ({
        useRoute: () => ({
            params: { id: '1' },
        }),
    }))
    
    describe('ViewCafe - teste de integração', () => {
        it('exibe os detalhes da cafeteria corretamente', async () => {
            const wrapper = mount(ViewCafe, {
                global: {
                    stubs: ['router-link'],
                },
            })
    
            await wrapper.vm.$nextTick()
            await wrapper.vm.$nextTick()
    
            expect(wrapper.text()).toContain('Café Teste')
            expect(wrapper.text()).toContain('20/07/2023')
            expect(wrapper.text()).toContain('Rua Teste, 123')
            expect(wrapper.text()).toContain('8.5')
        })
    })
    
  9. Executar testes:
    • Todos testes via terminal CLI: npx vitest
    • Todos testes com relatório em HTML: npx vitest run --reporter=html
    • Testes unitários via terminal CLI: npx vitest run tests/unit
    • Testes de integração via terminal CLI: npx vitest --run tests/integration
    • Todos testes via interface gráfica GUI: npx vitest --ui
    • Testes unitários via interface gráfica GUI: npx vitest --ui tests/unit
    • Testes de integração via interface gráfica GUI: npx vitest --ui tests/integration

Testes E2E:

Testes End-to-End (E2E) para validar fluxo completo da aplicação RESTful fullstack, via Cypress.

  1. Pré-requisitos: executar servidores backend e frontend;
  2. Pasta raíz do projeto frontend, criar estrutura de pastas cypress e abrir Cypress GUI: npx cypress open
  3. Pasta raíz do projeto frontend, editar arquivo 'cypress.config.ts':
    
    import { defineConfig } from 'cypress'
    
    export default defineConfig({
        e2e: {
            baseUrl: 'http://localhost:5173',
            specPattern: 'cypress/e2e/**/*.cy.{js,ts}',
            supportFile: 'cypress/support/e2e.ts',
            viewportWidth: 1280,
            viewportHeight: 800,
            defaultCommandTimeout: 8000,
            video: true,
        },
    })
    
  4. Em frontend/cypress, criar diretório e2e/ para arquivos de testes E2E
  5. Em frontend/cypress/e2e/, criar arquivo view_cafe.cy.ts para teste E2E da página de detalhes da cafeteria:
    
    describe('Visualização de uma cafeteria', () => {
        it('deve exibir os detalhes corretamente', () => {
            cy.visit('http://localhost:5173/view/1')
            cy.contains('Cafeteria 1:')
            cy.contains('Nome:')
            cy.contains('Avaliação:')
            cy.contains('/10')
            cy.get('a').contains('Voltar').click()
            cy.url().should('eq', 'http://localhost:5173/')
        })
    })
    
  6. Executar testes: npx cypress run (em package.json, remover linha: type: "module")
    • Via terminal CLI: npx cypress run
    • Via interface gráfica GUI: npx cypress open

Pipelines CI/CD:

Configuração de pipelines (fluxos automatizadores) de integração contínua (CI) e entrega contínua (CD) para automatizar testes e deploy no GitHub Actions. Criação de repositório GitHub para armazenar projetos backend (paris_cafe) e frontend (cafe-frontend). Conteúdo do repositório "prj_cafe_fullstack", contendo Pipelines backend.yml e frontend.yml:

  • Repositório prj_cafe_fullstack
    • paris_cafe/
      • requirements.txt # junto ao manage.py
    • cafe-frontend/
    • .github/
      • workflows/
        • backend.yml
        • frontend.yml
  1. Criar conta no GitHub, cadastrar usuário e email no Git, vinculá-los a chave SSH .pub, e cadastrar chave no GitHub;
  2. No GitHub, criar repositório "prj_cafe_fullstack", e baixá-lo com git clone;
  3. Mover projetos paris_cafe e cafe-frontend para dentro da pasta do repositório "prj_cafe_fullstack";
  4. Em prj_cafe_fullstack, criar diretório .github/workflows/;
  5. Em paris_cafe/requirements.txt, informar dependências do backend:
    
    Django
    djangorestframework
    markdown
    django-filter
    django-cors-headers
    pytest
    pytest-django
    pytest-html
    model_bakery
    
  6. Em .github/workflows/, criar arquivo Pipeline backend.yml:
    
    name: Django Backend CI
    
    on:
        push:
            branches: [main]
        pull_request:
            branches: [main]
    
    jobs:
        testes-backend:
            runs-on: ubuntu-latest
    
            services:
                postgres:
                    image: postgres:15
                    env:
                        POSTGRES_USER: postgres
                        POSTGRES_PASSWORD: postgres
                        POSTGRES_DB: test_db
                    ports: ["5432:5432"]
                    options: >-
                        --health-cmd pg_isready
                        --health-interval 10s
                        --health-timeout 5s
                        --health-retries 5
    
            env:
                DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
                DJANGO_SETTINGS_MODULE: paris_cafe.settings
                PYTHONUNBUFFERED: 1
    
            steps:
                - name: Checkout o código
                  uses: actions/checkout@v4
    
                - name: Instalar Python
                  uses: actions/setup-python@v5
                  with:
                      python-version: "3.11"
    
                - name: Instalar dependências
                  working-directory: paris_cafe
                  run: |
                      python -m pip install --upgrade pip
                      pip install -r requirements.txt
    
                - name: Migrar banco de dados
                  working-directory: paris_cafe
                  run: |
                      python manage.py migrate
    
                - name: Executar testes com pytest
                  working-directory: paris_cafe
                  run: |
                      pytest --html=report.html
    
                - name: Upload do relatório de testes
                  uses: actions/upload-artifact@v4
                  with:
                      name: pytest-report
                      path: paris_cafe/report.html
    
  7. Em prj_cafe_fullstack, publicar repositório e Pipeline no GitHub:
    • git add .
    • git commit -m "Projetos inseridos e Pipeline backend criada"
    • git push
    • Conferir: git status
  8. No repositório no GitHub (GitHub Actions), conferir se Pipeline "testes-backend" foi criada e suas etapas executadas corretamente;
  9. Em .github/workflows/, criar arquivo Pipeline frontend.yml:
    
    name: Pipeline Frontend CI
    
    on:
        push:
            branches:
                - main
                - develop
        pull_request:
    
    jobs:
        testes-frontend:
            runs-on: ubuntu-latest
            steps:
                - name: Checkout do código
                  uses: actions/checkout@v3
    
                - name: Instalar Node.js
                  uses: actions/setup-node@v3
                  with:
                      node-version: "22"
    
                - name: Instalar dependências
                  run: npm install
                  working-directory: cafe-frontend
    
                - name: Executar testes unitários e de integração
                  run: npx vitest
                  working-directory: cafe-frontend
    
                - name: Gerar relatório de testes
                  run: npx vitest --reporter=html
                  working-directory: cafe-frontend
    
  10. Em prj_cafe_fullstack, publicar repositório e Pipeline no GitHub:
    • git add .
    • git commit -m "Pipeline frontend criada"
    • git push
    • Conferir: git status
  11. No repositório no GitHub (GitHub Actions), conferir se Pipeline "testes-frontend" foi criada e suas etapas executadas corretamente;
  12. Em .github/workflows/, criar arquivo Pipeline e2e.yml:
    
    name: Pipeline E2E
    
    on:
        push:
            branches:
                - main
        pull_request:
    
    jobs:
        testes-e2e:
            runs-on: ubuntu-latest
            services:
                postgres:
                    image: postgres:15
                    env:
                        POSTGRES_USER: postgres
                        POSTGRES_PASSWORD: postgres
                        POSTGRES_DB: test_db
                    ports:
                        - 5432:5432
                    options: >-
                        --health-cmd pg_isready
                        --health-interval 10s
                        --health-timeout 5s
                        --health-retries 5
    
            steps:
                - name: Fazer checkout do código
                  uses: actions/checkout@v3
    
                - name: Configurar Python 3.13
                  uses: actions/setup-python@v4
                  with:
                      python-version: "3.13"
    
                - name: Configurar Node.js 22
                  uses: actions/setup-node@v3
                  with:
                      node-version: "22"
    
                - name: Instalar dependências do backend
                  working-directory: ./paris_cafe
                  run: |
                      python -m pip install --upgrade pip
                      pip install -r requirements.txt
    
                - name: Executar migrações Django
                  working-directory: ./paris_cafe
                  env:
                      DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
                      DJANGO_SETTINGS_MODULE: paris_cafe.settings
                  run: |
                      python manage.py migrate
    
                - name: Instalar dependências frontend
                  working-directory: ./cafe-frontend
                  run: npm install
    
                - name: Subir backend em background
                  working-directory: ./paris_cafe
                  env:
                      DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db
                      DJANGO_SETTINGS_MODULE: paris_cafe.settings
                  run: |
                      nohup python manage.py runserver 0.0.0.0:8000 &
                
                - name: Subir frontend em background
                  working-directory: ./cafe-frontend
                  run: |
                      nohup npm run dev &
    
                - name: Executar testes E2E com Cypress
                  working-directory: ./cafe-frontend
                  env:
                      VITE_BACKEND_URL: http://127.0.0.1:8000
                  run: npx cypress run
    
  13. Em prj_cafe_fullstack, publicar repositório e Pipeline no GitHub:
    • git add .
    • git commit -m "Pipeline e2e criada"
    • git push
    • Conferir: git status
  14. No repositório no GitHub (GitHub Actions), conferir se Pipeline "testes-e2e" foi criada e suas etapas executadas corretamente.

Deploy na AWS:

Publicar projetos (deploy) na cloud Amazon Web Services (AWS), gratuitamente, via recursos AWS free tier. Criação de instância EC2 (máquina Ubuntu CLI) contendo projetos backend e frontend. Acesse em AWS EC2.

  1. Criar instância:
    • Nome: ubuntuDjangoTeste1
    • AMI: Ubuntu Server (nível gratuito)
    • Tipo: t3.micro (verificar se região é t2 ou t3 nível gratuito)
    • Criar par de chaves:
      • Nome: keyDjangoTeste1
      • Tipo: RSA
      • Formato: conforme SO de sua máquina
    • Rede:
      • Atribuir IP público automaticamente: habilitar
      • Grupo de segurança criado automaticamente, com regras:
        • Permitir tráfego HTTP qualquer lugar
        • Permitir tráfego HTTPS qualquer lugar
        • Permitir tráfego SSH qualquer lugar
    • Armazenamento: 8GB (nível gratuito)
  2. Criar grupo de segurança:
    • Em E2, Grupos de segurança, clique em Criar grupo de segurança
    • Nome: grupoSegurancaTeste1
    • Descrição: Grupo de seguranca para testes 1
    • Regras de entrada:
      • SSH: porta 22, IP 0.0.0.0/0
      • HTTP: porta 80, IP 0.0.0.0/0
      • Personalizado TCP: porta 8000 (porta Django), IP 0.0.0.0/0
      • Personalizado TCP: porta 5173 (porta Vue.js), IP 0.0.0.0/0
    • Criar grupo de segurança
  3. Listar instâncias, selecionar instância ubuntuDjangoTeste1, Ações, Segurança, alterar grupo de segurança para grupoSegurancaTeste1
  4. Confirmar Executar instância (guardar IPv4 para acesso futuro)
  5. Selecionar instância ubuntuDjangoTeste1, e Conectar CLI usando IP público:
    • sudo apt update && sudo apt install python3-pip npm python3-venv git -y && sudo pip3 install django djangorestframework markdown django-filter django-cors-headers pytest pytest-django pytest-html model_bakery --break-system-packages && sudo npm install -g @vue/cli
    • curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install nodejs -y
    • git clone https://github.com/mateusschwede/cafe_fullstack.git && cd cafe_fullstack/paris_cafe
    • python3 manage.py migrate
    • Editar settings.py: comando nano paris_cafe/settings.py
      
      ALLOWED_HOSTS = [
          'ec2-54-233-2-156.sa-east-1.compute.amazonaws.com', // IPv4 da instância EC2
          'localhost',
          '127.0.0.1',
      ]
      
    • cd ../ && python3 manage.py runserver 0.0.0.0:8000 &
      • Enter para sair do comando iterativo (servidor Django executando em background nohup)
    • cd ../cafe-frontend
    • sudo npm install
    • Editar vite.config.ts: comando nano vite.config.ts
      
      export default defineConfig({
          // outro conteúdo
          server: {
              host: '0.0.0.0',
              port: 5173,
              allowedHosts: [
                  'ec2-54-233-2-156.sa-east-1.compute.amazonaws.com' // IPv4 da instância EC2
              ]
          }
      })
      
    • Editar src/services/api.ts: comando nano src/services/api.ts
      
      // outro conteúdo
      baseURL: 'http://ec2-54-233-2-156.sa-east-1.compute.amazonaws.com:8000' // IPv4 da instância EC2
      
    • sudo npx vite --host
      • Acessar API REST backend Django: http://ipv4_aws:8000
      • Acessar projeto no Browser: http://ipv4_aws:5173
  6. Para evitar custos adicionais AWS, excluir: instância ubuntuDjangoTeste1, grupoSegurancaTeste1 e chave de acesso ubuntuDjangoTeste1-key.pem

Elaborado por Mateus Schwede
ubsocial.github.io