diff --git a/docs/SOLIDJS_INTEGRATION.md b/docs/SOLIDJS_INTEGRATION.md new file mode 100644 index 0000000..fd3b934 --- /dev/null +++ b/docs/SOLIDJS_INTEGRATION.md @@ -0,0 +1,112 @@ +# SolidJS Integration Guide + +This guide explains how to connect a SolidJS frontend application to the Go Modulith backend. + +## Architecture Overview + +The backend provides several entry points for frontend clients: +- **gRPC-Gateway (REST)**: Best for standard CRUD operations and when using lightweight HTTP clients. +- **GraphQL**: Best for complex data fetching and minimizing network requests. +- **WebSockets**: Used for real-time notifications and reactive updates. + +## 1. Connecting via gRPC-Gateway (REST) + +The gRPC-Gateway translates your gRPC services into standard RESTful JSON endpoints. + +### Fetching Data +You can use the native `fetch` API or any HTTP client like `axios`. + +```typescript +import { createResource } from "solid-js"; + +const fetchUsers = async () => { + const response = await fetch("http://localhost:8080/v1/users"); + return response.json(); +}; + +const [users] = createResource(fetchUsers); +``` + +### Sending Data (POST/PUT) +```typescript +const createUser = async (userData) => { + const response = await fetch("http://localhost:8080/v1/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(userData), + }); + return response.json(); +}; +``` + +## 2. Using GraphQL + +The backend exposes a GraphQL endpoint at `/graphql`. For SolidJS, we recommend using `@urql/solid` or `solid-apollo`. + +### Setup with URQL +```typescript +import { createClient, Provider } from "@urql/solid"; + +const client = createClient({ + url: "http://localhost:8080/graphql", +}); + +// Wrap your app with the Provider + + + +``` + +## 3. Real-time Updates via WebSockets + +The WebSocket endpoint is available at `/ws`. It supports authentication via the `access_token` cookie. + +```typescript +import { onMount, onCleanup } from "solid-js"; + +const setupWS = () => { + const ws = new WebSocket("ws://localhost:8080/ws"); + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + console.log("Real-time update:", data); + }; + + onCleanup(() => ws.close()); +}; +``` + +## 4. Authentication (JWT Cookies) + +The backend uses `HttpOnly` and `Secure` cookies for session management. +- **Auto-propagation**: Browsers automatically send these cookies with requests to the same domain. +- **CORS**: If your frontend is on a different port (e.g., `:3000`), ensure `CORS_ALLOWED_ORIGINS` in `.env` includes your frontend URL and `credentials: "include"` is set in your fetch/GraphQL client. + +### Fetch with Credentials +```typescript +const response = await fetch("http://localhost:8080/v1/auth/me", { + credentials: "include", +}); +``` + +## 5. TypeScript Type Safety + +For full type safety, you can generate TypeScript clients from the `.proto` definitions using `buf`. + +### Recommendation +Add the following to your `buf.gen.yaml` to generate Connect-ES or gRPC-Web clients: + +```yaml + - plugin: es + out: gen/es + - plugin: connect-es + out: gen/es +``` + +Then install the dependencies in your SolidJS project: +```bash +pnpm add @bufbuild/protobuf @connectrpc/connect @connectrpc/connect-web +``` + +--- +Refer to [WEBSOCKET_GUIDE.md](WEBSOCKET_GUIDE.md) for more details on event formats. diff --git a/web/solid-example/.gitignore b/web/solid-example/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/solid-example/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/solid-example/README.md b/web/solid-example/README.md new file mode 100644 index 0000000..167c567 --- /dev/null +++ b/web/solid-example/README.md @@ -0,0 +1,28 @@ +## Usage + +```bash +$ npm install # or pnpm install or yarn install +``` + +### Learn more on the [Solid Website](https://solidjs.com) and come chat with us on our [Discord](https://discord.com/invite/solidjs) + +## Available Scripts + +In the project directory, you can run: + +### `npm run dev` + +Runs the app in the development mode.
+Open [http://localhost:5173](http://localhost:5173) to view it in the browser. + +### `npm run build` + +Builds the app for production to the `dist` folder.
+It correctly bundles Solid in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.
+Your app is ready to be deployed! + +## Deployment + +Learn more about deploying your application with the [documentations](https://vite.dev/guide/static-deploy.html) diff --git a/web/solid-example/index.html b/web/solid-example/index.html new file mode 100644 index 0000000..e28d816 --- /dev/null +++ b/web/solid-example/index.html @@ -0,0 +1,13 @@ + + + + + + + solid-example + + +
+ + + diff --git a/web/solid-example/package.json b/web/solid-example/package.json new file mode 100644 index 0000000..e920581 --- /dev/null +++ b/web/solid-example/package.json @@ -0,0 +1,20 @@ +{ + "name": "solid-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "solid-js": "^1.9.11" + }, + "devDependencies": { + "@types/node": "^24.12.0", + "typescript": "~5.9.3", + "vite": "^8.0.1", + "vite-plugin-solid": "^2.11.11" + } +} diff --git a/web/solid-example/public/favicon.svg b/web/solid-example/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/web/solid-example/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/solid-example/public/icons.svg b/web/solid-example/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/web/solid-example/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/solid-example/src/App.css b/web/solid-example/src/App.css new file mode 100644 index 0000000..f98a955 --- /dev/null +++ b/web/solid-example/src/App.css @@ -0,0 +1,236 @@ +:root { + --primary: #6366f1; + --secondary: #06b6d4; + --bg-deep: #0f172a; + --bg-card: rgba(30, 41, 59, 0.7); + --text-main: #f8fafc; + --text-muted: #94a3b8; + --border: rgba(255, 255, 255, 0.1); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: var(--bg-deep); + color: var(--text-main); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +.app-container { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.glass-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 2rem; + background: rgba(15, 23, 42, 0.8); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 100; +} + +.logo-group { + display: flex; + gap: 0.75rem; +} + +.logo { + height: 2rem; + transition: transform 0.3s ease; +} + +.logo:hover { + transform: scale(1.1) rotate(-5deg); +} + +.user-badge { + display: flex; + align-items: center; + gap: 0.75rem; + background: rgba(255, 255, 255, 0.05); + padding: 0.4rem 0.8rem; + border-radius: 2rem; +} + +.user-name { + font-weight: 500; + font-size: 0.9rem; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background: #94a3b8; +} + +.connected { background: #22c55e; box-shadow: 0 0 8px #22c55e; } +.disconnected { background: #ef4444; } + +main { + flex: 1; + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + width: 100%; +} + +.hero-section { + display: grid; + grid-template-columns: 1.2fr 1fr; + gap: 3rem; + align-items: center; + margin-bottom: 4rem; +} + +.hero-image-container { + position: relative; + border-radius: 1.5rem; + overflow: hidden; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); +} + +.hero-image { + width: 100%; + display: block; +} + +.hero-overlay { + position: absolute; + inset: 0; + background: linear-gradient(45deg, rgba(99, 102, 241, 0.2), transparent); +} + +.hero-content h2 { + font-size: 3rem; + font-weight: 800; + letter-spacing: -0.02em; + margin-bottom: 1rem; + background: linear-gradient(to right, #fff, #6366f1); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.hero-content p { + font-size: 1.125rem; + color: var(--text-muted); + margin-bottom: 2rem; +} + +.action-buttons { + display: flex; + gap: 1rem; +} + +.btn { + padding: 0.75rem 1.5rem; + border-radius: 0.75rem; + font-weight: 600; + text-decoration: none; + transition: all 0.2s ease; + cursor: pointer; + border: none; +} + +.btn.primary { + background: var(--primary); + color: white; +} + +.btn.primary:hover { + background: #4f46e5; + transform: translateY(-2px); +} + +.btn.secondary { + background: rgba(255, 255, 255, 0.05); + color: white; + border: 1px solid var(--border); +} + +.btn.secondary:hover { + background: rgba(255, 255, 255, 0.1); +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; +} + +.card.glass { + background: var(--bg-card); + backdrop-filter: blur(8px); + border: 1px solid var(--border); + padding: 2rem; + border-radius: 1.25rem; + transition: transform 0.3s ease, border-color 0.3s ease; +} + +.card.glass:hover { + transform: translateY(-5px); + border-color: rgba(99, 102, 241, 0.4); +} + +.card h3 { + margin-bottom: 1rem; + font-size: 1.25rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.code-snippet { + background: #000; + padding: 0.75rem; + border-radius: 0.5rem; + margin-top: 1rem; + font-family: monospace; + font-size: 0.85rem; + color: var(--secondary); +} + +.event-feed { + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.event-item { + display: flex; + justify-content: space-between; + padding: 0.5rem 0.75rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 0.4rem; + font-size: 0.8rem; +} + +.event-item .type { color: var(--secondary); font-weight: 600; } +.event-item .time { color: var(--text-muted); } + +footer { + padding: 3rem; + text-align: center; + color: var(--text-muted); + font-size: 0.9rem; + border-top: 1px solid var(--border); +} + +@media (max-width: 768px) { + .hero-section { + grid-template-columns: 1fr; + } +} diff --git a/web/solid-example/src/App.tsx b/web/solid-example/src/App.tsx new file mode 100644 index 0000000..ac89b76 --- /dev/null +++ b/web/solid-example/src/App.tsx @@ -0,0 +1,100 @@ +import { createSignal, onMount, onCleanup, createResource, Show, For } from 'solid-js' +import solidLogo from './assets/solid.svg' +import viteLogo from './assets/vite.svg' +import heroImg from './assets/hero.png' +import { fetchCurrentUser, setupEventsWebSocket, User } from './api' +import './App.css' + +function App() { + const [user] = createResource(fetchCurrentUser) + const [events, setEvents] = createSignal([]) + const [wsStatus, setWsStatus] = createSignal<'connecting' | 'connected' | 'disconnected'>('connecting') + + onMount(() => { + const ws = setupEventsWebSocket((data) => { + setEvents((prev) => [data, ...prev].slice(0, 5)) + }) + + ws.onopen = () => setWsStatus('connected') + ws.onclose = () => setWsStatus('disconnected') + + onCleanup(() => ws.close()) + }) + + return ( +
+
+
+ + +
+

Go Modulith + SolidJS

+
+ Guest Mode}> + {(u) => {u().name}} + +
+
+
+ +
+
+
+ Modulith Architecture +
+
+
+

Seamless Integration

+

+ Connect your SolidJS frontend to a powerful Go backend with gRPC-Gateway, + GraphQL, and real-time WebSockets. +

+
+ View Template + +
+
+
+ +
+
+

gRPC Gateway (REST)

+

Lightweight JSON communication translated directly from your Protobuf definitions.

+
+ fetch('/v1/auth/me') +
+
+
+

GraphQL

+

Flexible data fetching with built-in subscriptions and deep hierarchy support.

+
+ query {'{'} me {'{'} name {'}'} {'}'} +
+
+
+

Real-time Bus

+

Live events pushed directly to your UI via the internal Event Bus and WebSockets.

+
+ 0} fallback={

Awaiting live events...

}> + + {(event) => ( +
+ {event.type} + {new Date().toLocaleTimeString()} +
+ )} +
+
+
+
+
+
+ +
+

Built with Go Modulith Template & SolidJS

+
+
+ ) +} + +export default App diff --git a/web/solid-example/src/api.ts b/web/solid-example/src/api.ts new file mode 100644 index 0000000..3bab906 --- /dev/null +++ b/web/solid-example/src/api.ts @@ -0,0 +1,40 @@ +export const API_BASE_URL = "http://localhost:8080"; +export const WS_BASE_URL = "ws://localhost:8080/ws"; + +export interface User { + id: string; + email: string; + name: string; +} + +export const fetchCurrentUser = async (): Promise => { + try { + const response = await fetch(`${API_BASE_URL}/v1/auth/me`, { + credentials: "include", + }); + if (!response.ok) return null; + return response.json(); + } catch (err) { + console.error("Auth check failed:", err); + return null; + } +}; + +export const setupEventsWebSocket = (onMessage: (data: any) => void) => { + const ws = new WebSocket(WS_BASE_URL); + + ws.onopen = () => console.log("Connected to Modulith WebSocket"); + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + onMessage(data); + } catch (err) { + console.error("Failed to parse WS message:", err); + } + }; + + ws.onclose = () => console.log("Disconnected from Modulith WebSocket"); + ws.onerror = (err) => console.error("WebSocket error:", err); + + return ws; +}; diff --git a/web/solid-example/src/assets/hero.png b/web/solid-example/src/assets/hero.png new file mode 100644 index 0000000..abeb7fa Binary files /dev/null and b/web/solid-example/src/assets/hero.png differ diff --git a/web/solid-example/src/assets/solid.svg b/web/solid-example/src/assets/solid.svg new file mode 100644 index 0000000..025aa30 --- /dev/null +++ b/web/solid-example/src/assets/solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/solid-example/src/assets/vite.svg b/web/solid-example/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/web/solid-example/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/web/solid-example/src/index.css b/web/solid-example/src/index.css new file mode 100644 index 0000000..2c84af0 --- /dev/null +++ b/web/solid-example/src/index.css @@ -0,0 +1,111 @@ +:root { + --text: #6b6375; + --text-h: #08060d; + --bg: #fff; + --border: #e5e4e7; + --code-bg: #f4f3ec; + --accent: #aa3bff; + --accent-bg: rgba(170, 59, 255, 0.1); + --accent-border: rgba(170, 59, 255, 0.5); + --social-bg: rgba(244, 243, 236, 0.5); + --shadow: + rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + + --sans: system-ui, 'Segoe UI', Roboto, sans-serif; + --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: ui-monospace, Consolas, monospace; + + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light dark; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: 16px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #9ca3af; + --text-h: #f3f4f6; + --bg: #16171d; + --border: #2e303a; + --code-bg: #1f2028; + --accent: #c084fc; + --accent-bg: rgba(192, 132, 252, 0.15); + --accent-border: rgba(192, 132, 252, 0.5); + --social-bg: rgba(47, 48, 58, 0.5); + --shadow: + rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + } + + #social .button-icon { + filter: invert(1) brightness(2); + } +} + +body { + margin: 0; +} + +#root { + width: 1126px; + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } +} +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } +} +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} diff --git a/web/solid-example/src/index.tsx b/web/solid-example/src/index.tsx new file mode 100644 index 0000000..f67cd20 --- /dev/null +++ b/web/solid-example/src/index.tsx @@ -0,0 +1,8 @@ +/* @refresh reload */ +import { render } from 'solid-js/web' +import './index.css' +import App from './App.tsx' + +const root = document.getElementById('root') + +render(() => , root!) diff --git a/web/solid-example/tsconfig.app.json b/web/solid-example/tsconfig.app.json new file mode 100644 index 0000000..4de71f3 --- /dev/null +++ b/web/solid-example/tsconfig.app.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2023", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/web/solid-example/tsconfig.json b/web/solid-example/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/web/solid-example/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web/solid-example/tsconfig.node.json b/web/solid-example/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/web/solid-example/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/solid-example/vite.config.ts b/web/solid-example/vite.config.ts new file mode 100644 index 0000000..4095d9b --- /dev/null +++ b/web/solid-example/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [solid()], +})