JCC Express

Getting Started

Frontend

Introduction

JCC Express MVC is a backend framework first: it gives you routing, controllers, middleware, validation, caching, queues, file storage, database access with an Eloquent-style ORM, authentication, and the rest of the machinery you expect from a serious web stack. At the same time, it is built so you can ship a cohesive full-stack experience—whether you prefer to render HTML on the server or to pair the server with a modern JavaScript UI.

There are two main ways to approach the frontend:

  1. Server-rendered HTML using Blade-style templates (your routes and controllers return HTML; the browser loads full pages on each navigation).
  2. Inertia.js with React or Vue (and Vite for assets), so you build interactive UIs in JavaScript while keeping routes, controllers, and data loading on the server—one repository, no bespoke JSON API for every screen.

Which you choose depends on how dynamic your product needs to feel and where your team is most productive. Both are first-class in this ecosystem; many applications use both (Inertia for the main app, Blade for admin tools, emails, or internal pages).


Server-rendered views and Blade

Classic server applications build HTML by merging templates with data fetched during the request. In PHP frameworks, that often meant raw HTML mixed with logic. In JCC Express MVC, the same idea is expressed through views and a Blade-like syntax: concise directives for loops, conditionals, escaping, and layouts.

Example — iterating over users in a Blade view (resources/views). Syntax matches JCC Blade (loops and {{ }} may use spaces around . like {{ user . name }}):

blade
<div>
  @foreach (users as user)
    Hello, {{ user . name }} <br />
  @endforeach
</div>

When you work this way, form posts and link clicks typically receive a new HTML document from the server and the browser performs a full navigation. That model is simple, SEO-friendly, and enough for dashboards, internal tools, landing pages, and many line-of-business apps.

Returning a Blade view from a route or controller:

TypeScript
return res.render("users/index", { users });
// or the global helper (same as res.render):
return view("users/index", { users });

See Directory structure for where resources/views and public/ live.

For engine configuration (app/Config/engine), dot-notation view paths, Route.view, and res.locals, see [Views](../The Basics/Views.md). For the JCC Blade engine (directives, performance, custom directives), see [JCC Blade](../The Basics/JCC-Blade.md).


When you need a richer, more dynamic UI

As expectations for web apps have grown, many teams want smoother interactions—fewer full reloads, snappier transitions, and rich client-side state—without giving up a single server-driven codebase.

In other ecosystems, that has led to JavaScript-heavy SPAs (often in a separate repo), or to backend-native solutions (for example Laravel Livewire). JCC Express MVC does not ship Livewire, but you can:

  • Keep improving Blade pages and add small amounts of JavaScript where needed (progressive enhancement), or
  • Adopt Inertia (below) when the product should feel like a modern single-page app while your routes and controllers stay on the server.

Inertia with React or Vue

Pairing a backend framework with React or Vue without extra glue usually forces you to solve client routing, data hydration, authentication, and deployment twice. Inertia removes that split: you still define URLs on the server, load data in controllers or route handlers, and return an Inertia page instead of a Blade document. The browser runs your React or Vue page component and receives props from the server—no parallel REST API for every screen, and one codebase to version and deploy.

Server — return an Inertia page (names map to files under resources/js/Pages/):

TypeScript
import { User } from "@/Models/User";

Route.get("/users/:id", async (req, res) => {
  const user = await User.find(req.params.id);
  return res.inertia("users/show", { user });
  // or: return inertia("users/show", { user });
});

The global inertia() helper calls res.inertia using the current response from the app container—same behavior, different style.

Client — a React page component receives those props (paths and extensions match your starter; often resources/js/Pages/users/show.jsx):

tsx
import { Head } from "@inertiajs/react";

export default function Show({ user }) {
  return (
    <>
      <Head title="Profile" />
      <h1>Welcome</h1>
      <p>Hello {user.name}, welcome to Inertia.</p>
    </>
  );
}

Use <Link> and the router from @inertiajs/react (or the Vue equivalents) so navigations stay inside Inertia. CSRF and method spoofing are handled by middleware you register in app/Http/kernel.ts alongside the Inertia middleware.


Redirects

Normal HTTP redirects — use Express on res or the global redirect helper (same outcome):

TypeScript
return res.redirect(303, "/");
// or, with default status 303:
return res.redirect("/");

return redirect("/");
// optional second argument is the status code:
return redirect("/", 303);

The redirect(url, status?) helper forwards to res.redirect. If you omit the status, it uses 303, which is appropriate for redirect-after-POST and works well when you want a full page navigation (for example leaving an Inertia flow or forcing a traditional reload).

Inertia-aware redirects — when the request is an Inertia XHR and you want the client router to follow a new URL without a full document load, use res.inertiaRedirect(url) (see Responses at a glance below).


Inertia middleware and root layout

Register Inertia in your HTTP kernel so res.inertia / inertia() exist and the first load receives the correct HTML shell:

TypeScript
import { inertia } from "jcc-express-mvc/Core/Inertia";

export class Kernel {
  public middlewares = [
    // …csrf, methodSpoofing, etc.
    inertia({ rootView: "welcome", ssr: true }),
  ];
}
  • rootView — Blade file under resources/views (for example welcome.blade.html) that includes @vite and @inertia so Vite assets and the client mount point are present.
  • ssr — Enable or configure server-side rendering when your stack supports it (see Server-side rendering (SSR) below and Deployment).

app/Config/engine.ts sets the views root and template extension.


Server-side rendering (SSR)

If you need server-rendered HTML for the initial Inertia load (SEO, perceived performance, or accessibility), Inertia supports SSR. The reference app includes an ssr entry (for example resources/js/ssr.jsx) built with vite build --ssr. Combine that with your project’s vite-build script and any bun artisanNode inertia:start-ssr (or ssr:server) workflow described in Deployment.


Starter kits

To start with Inertia + React or Inertia + Vue (with or without TypeScript), use jcc-express-starter. After npx or bunx jcc-express-starter my-app, the CLI asks you to choose a frontend stack; it then scaffolds resources/js, vite.config, dependencies, and a working Kernel pattern so you can run watch and dev (see Installation).

For Blade-heavy apps, you can still use the same starter: keep Inertia registered for some routes and use view() / res.render() for others.


Vite and asset bundling

Whether you lean on Blade or Inertia + React/Vue, you will usually bundle CSS (and any JavaScript) for production. Inertia projects also bundle page components into browser-ready assets.

Vite is the default bundler: fast builds and Hot Module Replacement (HMR) during development. The Laravel Vite plugin (laravel-vite-plugin) wires Vite to your Blade templates (@vite, @viteReactRefresh) and to the expected resources/css and resources/js entry points. Your vite.config.mjs (or .ts) lists those entries and, when applicable, an ssr input.

For Blade tags, public/hot, manifest resolution, and troubleshooting, see [Asset bundling](../The Basics/Asset-bundling.md).

Local workflow — use two terminals:

Bash
# Terminal 1 — backend (recommended: Laravel-style watcher)
bun artisanNode serve

# Terminal 2 — Vite assets (writes public/hot)
bun run watch   # or: npm run watch

Alternative backend command:

Bash
bun run dev     # or: npm run dev  → bun --watch server.ts (simpler, no serve watcher)

Production-style build:

Bash
bun run vite-build   # typically: vite build && vite build --ssr

Tailwind CSS (often v4 with @tailwindcss/vite) usually starts from resources/css/app.css with @import "tailwindcss" and @source paths so classes in .blade.html and resources/js are included.


Responses at a glance

  • Blade view — res.render(name, data) or view(name, data) (same call).
  • Inertia page — res.inertia(component, props, option?) or inertia(component, props, option?) (same call).
  • Full page redirect — res.redirect(status, url) / res.redirect(url), or redirect(url, status?) (helper defaults to 303).
  • Inertia redirect — res.inertiaRedirect(url) for navigations that should stay inside Inertia (no global redirect alias for this).

Forms and navigation should use Inertia-aware clients where applicable; traditional forms can still post to Blade routes.