Stack Vue.js — InfoWhere
Template de stack para projetos Vue.js
Última atualização: 25/01/2026
Uso: Principal (projetos pessoais InfoWhere)
1. Visão Geral
Stack frontend principal para projetos InfoWhere e side projects.
Filosofia
- Bootstrap como UI framework (familiar, produtivo)
- Componentes pragmáticos — dividir quando faz sentido, não por dogma
- Composition API com
ref(),computed(),watch() - Pinia para estado global
- Diretivas quando simplificam o código
2. Versões
| Componente | Versão | Notas |
|---|---|---|
| Vue | 3.5.x | Composition API |
| TypeScript | 5.x | Sempre usar TypeScript |
| Vite | 6.x | Build tool |
| pnpm | Última | Gerenciador de pacotes |
3. Dependências Core
3.1 Framework Base
{
"dependencies": {
"vue": "^3.5.0",
"vue-router": "^4.4.0",
"pinia": "^2.2.0"
},
"devDependencies": {
"typescript": "^5.6.0",
"vite": "^6.0.0",
"@vitejs/plugin-vue": "^5.2.0",
"vue-tsc": "^2.1.0"
}
}
3.2 UI Framework
{
"dependencies": {
"bootstrap": "^5.3.0",
"@popperjs/core": "^2.11.0"
}
}
Nota: Bootstrap é o padrão. PrimeVue só se precisar de componentes complexos (data tables, calendários, etc.).
### 3.3 HTTP Client
```json
{
"dependencies": {
"axios": "^1.7.0"
}
}
3.4 Autenticação
{
"dependencies": {
"keycloak-js": "^26.0.0"
}
}
3.5 Utilitários
{
"dependencies": {
"vee-validate": "^4.14.0",
"yup": "^1.4.0",
"@vueuse/core": "^11.3.0",
"date-fns": "^4.1.0"
}
}
4. Testes
{
"devDependencies": {
"vitest": "^2.1.0",
"@vue/test-utils": "^2.4.0",
"@testing-library/vue": "^8.1.0",
"jsdom": "^25.0.0",
"@vitest/coverage-v8": "^2.1.0"
}
}
Cobertura mínima: 70%
5. Autenticação
| Componente | Escolha |
|---|---|
| Identity Provider | Keycloak |
| Lib | keycloak-js |
| Storage | Memory (não localStorage) |
Configuração típica
// src/config/keycloak.ts
import Keycloak from 'keycloak-js';
export const keycloak = new Keycloak({
url: import.meta.env.VITE_KEYCLOAK_URL,
realm: import.meta.env.VITE_KEYCLOAK_REALM,
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID,
});
export async function initKeycloak(): Promise<boolean> {
try {
const authenticated = await keycloak.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
pkceMethod: 'S256',
});
return authenticated;
} catch (error) {
console.error('Keycloak init failed', error);
return false;
}
}
Axios Interceptor
// src/config/axios.ts
import axios from 'axios';
import { keycloak } from './keycloak';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
});
api.interceptors.request.use(async (config) => {
if (keycloak.token) {
// Refresh token if expiring in 30 seconds
if (keycloak.isTokenExpired(30)) {
await keycloak.updateToken(30);
}
config.headers.Authorization = `Bearer ${keycloak.token}`;
}
return config;
});
export default api;
6. Estrutura do Projeto
{projeto}/
├── public/
│ ├── favicon.ico
│ └── silent-check-sso.html # Keycloak SSO
├── src/
│ ├── main.ts # Entry point
│ ├── App.vue
│ ├── assets/
│ │ └── styles/
│ │ ├── main.scss
│ │ └── _variables.scss
│ ├── components/
│ │ ├── common/ # Reusable components
│ │ │ ├── AppHeader.vue
│ │ │ ├── AppFooter.vue
│ │ │ └── AppLoading.vue
│ │ └── features/ # Feature-specific
│ │ └── users/
│ │ └── UserCard.vue
│ ├── composables/ # Composition functions
│ │ ├── useAuth.ts
│ │ └── useApi.ts
│ ├── config/
│ │ ├── keycloak.ts
│ │ └── axios.ts
│ ├── layouts/
│ │ ├── DefaultLayout.vue
│ │ └── AuthLayout.vue
│ ├── pages/ # Route pages
│ │ ├── HomePage.vue
│ │ ├── LoginPage.vue
│ │ └── users/
│ │ ├── UserListPage.vue
│ │ └── UserDetailPage.vue
│ ├── router/
│ │ ├── index.ts
│ │ └── guards.ts
│ ├── stores/ # Pinia stores
│ │ ├── index.ts
│ │ ├── auth.store.ts
│ │ └── user.store.ts
│ ├── services/ # API services
│ │ └── user.service.ts
│ ├── types/
│ │ └── index.ts
│ └── utils/
│ └── formatters.ts
├── tests/
│ ├── setup.ts
│ ├── unit/
│ └── components/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── .env.example
└── README.md
7. Padrões e Convenções
7.1 Componentes — Abordagem Pragmática
Princípio: Dividir quando faz sentido, não por dogma.
| Quando criar componente separado | Quando NÃO separar |
|---|---|
| Reutilizado em 2+ lugares | Usado só uma vez e simples |
| Lógica complexa isolada | Poucos elementos HTML |
| Testável independentemente | Acoplado demais ao pai |
# BOM: Componentes com propósito claro
components/
├── common/
│ ├── AppButton.vue # Reutilizado em tudo
│ ├── AppModal.vue # Reutilizado em tudo
│ └── AppDataTable.vue # Complexo, isola lógica
├── UserCard.vue # Reutilizado em listas
└── InvoiceForm.vue # Complexo, isola lógica
# EVITAR: Dividir demais
components/
├── UserCardHeader.vue # Só usado dentro de UserCard
├── UserCardBody.vue # Só usado dentro de UserCard
├── UserCardFooter.vue # Só usado dentro de UserCard
└── UserCardAvatar.vue # 3 linhas de HTML
7.2 Reatividade (ref, computed, watch)
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
// ref() — valores reativos
const count = ref(0);
const user = ref<User | null>(null);
// computed() — valores derivados (recalculam automaticamente)
const double = computed(() => count.value * 2);
const fullName = computed(() =>
user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
);
// watch() — reagir a mudanças (efeitos colaterais)
watch(user, (newUser) => {
if (newUser) {
console.log('Usuário mudou:', newUser.id);
}
});
// Métodos
function increment() {
count.value++;
}
</script>
Regra: Usar computed() sempre que um valor deriva de outro. Evita recálculos desnecessários.
7.3 Componentes
- Naming:
PascalCase(UserCard.vue) - Composition API: Sempre usar
<script setup lang="ts"> - Props: Sempre tipadas com
defineProps<T>() - Emits: Sempre tipados com
defineEmits<T>()
<script setup lang="ts">
interface Props {
userId: string;
showDetails?: boolean;
}
interface Emits {
(e: 'select', id: string): void;
(e: 'delete', id: string): void;
}
const props = withDefaults(defineProps<Props>(), {
showDetails: false,
});
const emit = defineEmits<Emits>();
</script>
<template>
<div class="user-card">
<!-- content -->
</div>
</template>
<style scoped lang="scss">
.user-card {
// styles
}
</style>
7.2 Composables
// src/composables/useUsers.ts
import { ref, computed } from 'vue';
import { userService } from '@/services/user.service';
import type { User } from '@/types';
export function useUsers() {
const users = ref<User[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
const activeUsers = computed(() =>
users.value.filter(u => u.active)
);
async function fetchUsers() {
loading.value = true;
error.value = null;
try {
users.value = await userService.getAll();
} catch (e) {
error.value = 'Failed to fetch users';
} finally {
loading.value = false;
}
}
return {
users,
loading,
error,
activeUsers,
fetchUsers,
};
}
7.3 Stores (Pinia)
// src/stores/auth.store.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { keycloak } from '@/config/keycloak';
export const useAuthStore = defineStore('auth', () => {
const user = ref<KeycloakProfile | null>(null);
const isAuthenticated = computed(() => !!user.value);
async function login() {
await keycloak.login();
}
async function logout() {
await keycloak.logout();
user.value = null;
}
return {
user,
isAuthenticated,
login,
logout,
};
});
7.4 Router Guards
// src/router/guards.ts
import type { NavigationGuard } from 'vue-router';
import { useAuthStore } from '@/stores/auth.store';
export const authGuard: NavigationGuard = (to, from, next) => {
const authStore = useAuthStore();
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next({ name: 'login', query: { redirect: to.fullPath } });
} else {
next();
}
};
8. Infraestrutura
| Componente | Escolha |
|---|---|
| Build | Vite |
| Deploy | Nginx / Cloudflare Pages |
| CI/CD | GitHub Actions |
Dockerfile típico
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Production stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
}
}
9. O que NÃO usar
| Tecnologia | Motivo |
|---|---|
| JavaScript puro | Sempre TypeScript |
| Options API | Preferir Composition API |
| Vuex | Preferir Pinia |
| Vue CLI | Preferir Vite |
| Tailwind | Preferir Bootstrap (mais familiar) |
| localStorage para tokens | Security risk |
10. Checklist de Novo Projeto
- Criar projeto com
pnpm create vue@latest - Selecionar: TypeScript, Vue Router, Pinia, Vitest
- Instalar Bootstrap ou PrimeVue
- Configurar Keycloak
- Configurar Axios com interceptors
- Criar estrutura de pastas
- Configurar router guards
- Criar layouts base
- Configurar variáveis de ambiente
- Criar Dockerfile + nginx.conf
- Criar README.md com instruções
11. Links de Referência
Nota: Este template é a base. Cada projeto pode ter ajustes específicos documentados no
technical_context.mddo projeto.