Skip to main content

Frontend Architecture

This guide explains Saucebase's frontend architecture, component structure, and build system.

Architecture Overview

Entry Points

Client-Side Rendering (app.ts)

Main entry point for browser rendering:

// resources/js/app.ts
import { createApp, h, DefineComponent } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
import { resolveModularPageComponent } from './lib/utils';
import { i18nVue } from 'laravel-vue-i18n';
import { setupModules, afterMountModules } from './lib/moduleSetup';

createInertiaApp({
resolve: (name) => resolveModularPageComponent(name, import.meta.glob<DefineComponent>([
'./pages/**/*.vue',
'../../../modules/*/resources/js/pages/**/*.vue'
])),

setup({ el, App, props, plugin }) {
const app = createApp({ render: () => h(App, props) });

// Register plugins
app.use(plugin);
app.use(ZiggyVue);
app.use(i18nVue, {
resolve: async (lang: string) => {
const langs = import.meta.glob<{ default: any }>('../../lang/*.json');
return await langs[`../../lang/${lang}.json`]();
},
});

// Module setup lifecycle
setupModules(app);

// Mount app
app.mount(el);

// Post-mount lifecycle
afterMountModules(app);

return app;
},
});

Server-Side Rendering (ssr.ts)

Entry point for SSR server:

// resources/js/ssr.ts
import { createSSRApp, h, DefineComponent } from 'vue';
import { renderToString } from '@vue/server-renderer';
import { createInertiaApp } from '@inertiajs/vue3';
import createServer from '@inertiajs/vue3/server';
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
import { resolveModularPageComponent } from './lib/utils';
import { i18nVue } from 'laravel-vue-i18n';
import { setupModules } from './lib/moduleSetup';

createServer((page) =>
createInertiaApp({
page,
render: renderToString,
resolve: (name) => resolveModularPageComponent(name, import.meta.glob<DefineComponent>([
'./pages/**/*.vue',
'../../../modules/*/resources/js/pages/**/*.vue'
])),

setup({ App, props, plugin }) {
const app = createSSRApp({ render: () => h(App, props) });

// Register plugins
app.use(plugin);
app.use(ZiggyVue, {
...page.props.ziggy,
location: new URL(page.props.ziggy.location),
});
app.use(i18nVue, {
resolve: (lang: string) => {
const langs = import.meta.glob<{ default: any }>('../../lang/*.json', { eager: true });
return langs[`../../lang/${lang}.json`].default;
},
});

// Module setup (no afterMount in SSR)
setupModules(app);

return app;
},
})
);

Inertia Props Flow

Data flows from Laravel controllers to Vue components as typed props:

Backend (Controller):

return Inertia::render('Users/Index', [
'users' => User::with('roles')->paginate(10),
'filters' => $request->only(['search', 'role']),
]);

Frontend (Vue Component):

<script setup lang="ts">
interface Props {
users: PaginatedData<User>;
filters: { search?: string; role?: string };
}

const props = defineProps<Props>();
</script>

The props are type-safe - TypeScript validates that the component receives the expected data structure. Changes to the backend immediately show type errors in the frontend.

Module Page Resolution

Saucebase extends Inertia to support modular architecture with namespace syntax.

Resolution Logic

// resources/js/lib/utils.ts
import type { DefineComponent } from 'vue';

export async function resolveModularPageComponent(
name: string,
pages: Record<string, () => Promise<DefineComponent>>
): Promise<DefineComponent> {
// Check for module namespace syntax (Module::Page)
if (name.includes('::')) {
const [moduleName, pagePath] = name.split('::');
const path = `../../../modules/${moduleName}/resources/js/pages/${pagePath}.vue`;

const resolvedPage = pages[path];
if (!resolvedPage) {
throw new Error(`Page component not found: ${path}`);
}

return (await resolvedPage()).default;
}

// Core pages
const path = `./pages/${name}.vue`;
const resolvedPage = pages[path];

if (!resolvedPage) {
throw new Error(`Page component not found: ${path}`);
}

return (await resolvedPage()).default;
}

Usage in Controllers

// Core page
return Inertia::render('Dashboard');
// Resolves to: resources/js/pages/Dashboard.vue

// Module page
return Inertia::render('Auth::Login');
// Resolves to: modules/Auth/resources/js/pages/Login.vue

This namespace syntax keeps module pages isolated while maintaining simple, readable controller code.

Learn more: Modules Guide for practical usage examples.

Module Lifecycle

Modules can export setup hooks for initialization:

// modules/Auth/resources/js/app.ts
import type { App } from 'vue';

export default {
// Called before app mounts (both CSR and SSR)
setup(app: App) {
// Register plugins, components, directives
app.component('CustomButton', CustomButton);
app.directive('focus', focusDirective);
},

// Called after app mounts (CSR only, not SSR)
afterMount(app: App) {
// Initialize services that require DOM
initAnalytics();
initWebSocket();
},
};

Module Setup Orchestration

// resources/js/lib/moduleSetup.ts
import type { App } from 'vue';

// Dynamically import module setup files
const moduleSetups = import.meta.glob<{ default: any }>(
'../../../modules/*/resources/js/app.ts',
{ eager: true }
);

export function setupModules(app: App) {
Object.values(moduleSetups).forEach((moduleSetup) => {
if (moduleSetup.default?.setup) {
moduleSetup.default.setup(app);
}
});
}

export function afterMountModules(app: App) {
Object.values(moduleSetups).forEach((moduleSetup) => {
if (moduleSetup.default?.afterMount) {
moduleSetup.default.afterMount(app);
}
});
}

State Management

Saucebase doesn't include a global state management library by default. Use these patterns:

Create reusable stateful logic with composables:

// resources/js/composables/useAuth.ts
import { ref, computed } from 'vue';
import { usePage } from '@inertiajs/vue3';

export function useAuth() {
const page = usePage();

const user = computed(() => page.props.auth?.user);
const isAuthenticated = computed(() => !!user.value);

return {
user,
isAuthenticated,
};
}

Usage:

<script setup lang="ts">
import { useAuth } from '@/composables/useAuth';

const { user, isAuthenticated } = useAuth();
</script>

<template>
<div v-if="isAuthenticated">
Welcome, {{ user.name }}!
</div>
</template>

2. Shared Inertia Props

Share data globally via Inertia middleware:

// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
],
'flash' => [
'success' => $request->session()->get('success'),
'error' => $request->session()->get('error'),
],
'locale' => app()->getLocale(),
];
}

Access in any component:

<script setup lang="ts">
import { usePage } from '@inertiajs/vue3';

const page = usePage();
const flash = computed(() => page.props.flash);
const locale = computed(() => page.props.locale);
</script>

Internationalization (i18n)

Translation Files

lang/
├── en.json
│ {
│ "welcome": "Welcome",
│ "logout": "Logout"
│ }
└── pt_BR.json
{
"welcome": "Bem-vindo",
"logout": "Sair"
}

Usage in Components

<script setup lang="ts">
import { trans } from 'laravel-vue-i18n';
</script>

<template>
<div>
<h1>{{ trans('welcome') }}</h1>
<button>{{ trans('logout') }}</button>
</div>
</template>

Change Locale

<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { loadLanguageAsync } from 'laravel-vue-i18n';

const changeLocale = async (locale: string) => {
await loadLanguageAsync(locale);
router.visit(route('locale', { locale }));
};
</script>

<template>
<button @click="changeLocale('en')">English</button>
<button @click="changeLocale('pt_BR')">Português</button>
</template>

Build System (Vite)

Configuration

// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
import { collectModuleAssetsPaths, collectModuleLangPaths } from './module-loader.js';

export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.ts', ...collectModuleAssetsPaths()],
ssr: 'resources/js/ssr.ts',
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
resolve: {
alias: {
'@': '/resources/js',
'@modules': '/modules/',
'ziggy-js': '/vendor/tightenco/ziggy',
},
},
ssr: {
noExternal: ['laravel-vue-i18n'],
},
});

Module Asset Collection

The module-loader.js automatically discovers and includes enabled module assets:

// module-loader.js
export function collectModuleAssetsPaths() {
const enabledModules = getEnabledModules();
const assetPaths = [];

enabledModules.forEach((moduleName) => {
const configPath = `./modules/${moduleName}/vite.config.js`;
if (fs.existsSync(configPath)) {
const config = require(configPath);
const paths = config.default?.paths || [];
paths.forEach((path) => {
assetPaths.push(`modules/${moduleName}/resources/${path}`);
});
}
});

return assetPaths;
}

Development vs Production

# Development (HMR enabled)
npm run dev

# Production build (optimized, minified)
npm run build

# SSR build
npm run build:ssr

TypeScript Integration

Type Definitions

// resources/js/types/global.d.ts
import { PageProps as InertiaPageProps } from '@inertiajs/core';
import { AxiosInstance } from 'axios';
import { route as ziggyRoute } from 'ziggy-js';

declare global {
interface Window {
axios: AxiosInstance;
}

var route: typeof ziggyRoute;
}

export interface User {
id: number;
name: string;
email: string;
email_verified_at?: string;
}

export interface Auth {
user: User | null;
}

export type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
auth: Auth;
flash: {
success?: string;
error?: string;
};
locale: string;
ziggy: {
location: string;
query: Record<string, any>;
};
};

declare module 'vue' {
interface ComponentCustomProperties {
route: typeof ziggyRoute;
}
}

Using Types in Components

<script setup lang="ts">
import type { PageProps, User } from '@/types/global';

interface Props {
users: User[];
}

const props = defineProps<Props>();

// Type-safe page props
import { usePage } from '@inertiajs/vue3';
const page = usePage<PageProps>();

// Auto-completion works!
const user = page.props.auth.user;
const flash = page.props.flash;
</script>

Next Steps