Coherent.js Full-Stack Guide
Coherent.js is a full-stack JavaScript framework that combines server-side rendering (SSR) with progressive client-side enhancement.
🌊 The Full-Stack Flow
1️⃣ Server Renders (SSR)
The server generates HTML using @coherent.js/core:
// server.js
import express from 'express';
import { render } from '@coherent.js/core';
import { provideContext } from '@coherent.js/state';
const app = express();
app.get('/users/:id', async (req, res) => {
// Fetch data on server
const user = await db.users.findById(req.params.id);
// Provide context for SSR
provideContext('user', user);
// Render to HTML
const html = render(UserProfilePage());
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>User Profile</title>
<script src="/bundle.js" defer></script>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`);
});
Result: Fast initial page load with SEO-friendly HTML
2️⃣ Client Hydrates (Progressive Enhancement)
The browser makes the server HTML interactive:
// client.js
import { hydrateBySelector } from '@coherent.js/client';
import { UserProfilePage } from './components/UserProfilePage.js';
// Attach event listeners to server-rendered HTML
hydrateBySelector('#app', UserProfilePage);
Result: Server HTML becomes fully interactive
3️⃣ Client Enhances (SPA Features)
After hydration, add reactive features:
// client.js
import { hydrate } from '@coherent.js/client';
import { createRouter } from '@coherent.js/client/router';
import { createReactiveState } from '@coherent.js/state';
// Hydrate first
hydrateBySelector('#app', UserProfilePage);
// Enable client-side routing (SPA-like navigation)
const router = createRouter({
mode: 'history',
routes: {
'/': HomePage,
'/users/:id': UserProfilePage,
'/products': ProductsPage
}
});
// Add reactive state for interactions
const appState = createReactiveState({
cart: [],
notifications: []
});
appState.watch('cart', (newCart) => {
console.log('Cart updated:', newCart);
// Update UI reactively
});
router.start('#app');
Result: SPA-like experience with instant navigation
🎯 Complete Example: Full-Stack App
Server (SSR)
// server/index.js
import express from 'express';
import { render } from '@coherent.js/core';
import { provideContext } from '@coherent.js/state';
import { createFormBuilder, validators } from '@coherent.js/forms';
const app = express();
// SSR route with form
app.get('/signup', (req, res) => {
// Build form on server with validation metadata
const signupForm = createFormBuilder({ name: 'signup' })
.field('email', {
type: 'email',
label: 'Email',
required: true,
validators: ['email'] // Embedded in HTML as data-validators="email"
})
.field('password', {
type: 'password',
label: 'Password',
required: true,
validators: ['minLength:8'] // Embedded as data-validators="minLength:8"
});
// Render form to HTML (includes validation metadata)
const html = render({
div: {
children: [
{ h1: { text: 'Sign Up' } },
signupForm.buildForm()
]
}
});
res.send(wrapHTML(html, '/bundle.js'));
});
// Handle form submission (server-side validation)
app.post('/signup', express.json(), async (req, res) => {
// Validate with SAME rules as client
const { valid, errors } = validators.validate(req.body, {
email: ['required', 'email'],
password: ['required', 'minLength:8']
});
if (!valid) {
return res.status(400).json({ errors });
}
const user = await db.users.create(req.body);
res.json({ user });
});
app.listen(3000);
Client (Hydration + Enhancement)
// client/index.js
import { hydrateBySelector } from '@coherent.js/client';
import { createRouter } from '@coherent.js/client/router';
import { createReactiveState, withLocalStorage } from '@coherent.js/state';
import { hydrateForm } from '@coherent.js/forms';
// 1. Hydrate server HTML
hydrateBySelector('#app', App);
// 2. Set up client-side routing
const router = createRouter({
mode: 'history',
routes: {
'/': HomePage,
'/products': ProductsPage,
'/cart': CartPage
},
prefetch: {
enabled: true,
strategy: 'hover' // Prefetch on hover
}
});
// 3. Set up reactive state
const cartState = withLocalStorage(
createReactiveState({ items: [], total: 0 }),
'cart'
);
cartState.watch('items', async (items) => {
// Sync with server
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ items })
});
});
// 4. Hydrate server-rendered forms (progressive enhancement)
const signupForm = hydrateForm('form[name="signup"]', {
validateOnBlur: true,
validateOnChange: false,
debounce: 300,
onSubmit: async (data, event) => {
// Client-side submission with fetch
const response = await fetch('/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const { errors } = await response.json();
console.error('Validation errors:', errors);
return false; // Don't submit form
}
const { user } = await response.json();
console.log('User created:', user);
router.push('/dashboard');
},
onError: (errors) => {
console.error('Form errors:', errors);
}
});
// ^^^ Reads data-validators from HTML
// Attaches event listeners
// No duplication - enhances server HTML!
// Start the router
router.start('#app');
Components (Universal - Works on Both Server and Client)
// components/ProductsPage.js
import { useContext } from '@coherent.js/state';
export function ProductsPage() {
// On server: reads from context provided during SSR
// On client: reads from reactive state
const products = useContext('products') || [];
return {
div: {
className: 'products-page',
children: [
{ h1: { text: 'Products' } },
{
div: {
className: 'product-grid',
children: products.map(product => ({
div: {
className: 'product-card',
children: [
{ h3: { text: product.name } },
{ p: { text: `${product.price}` } },
{
button: {
text: 'Add to Cart',
onclick: `addToCart(${product.id})`
}
}
]
}
}))
}
}
]
}
};
}
📊 Architecture Comparison
Traditional SPA (React, Vue)
┌─────────────────┐
│ Browser │
│ - Blank HTML │ ← Initial load is slow
│ - JS loads │ ← Downloads framework
│ - App renders │ ← Client-side only
└─────────────────┘
❌ Slow initial load ❌ Poor SEO ✅ Fast subsequent navigation
Traditional SSR (PHP, Rails)
┌─────────────────┐
│ Server │
│ - Renders HTML │ ← Fast initial load
│ - Full reload │ ← Every navigation is slow
└─────────────────┘
✅ Fast initial load ✅ Good SEO ❌ Slow subsequent navigation
Coherent.js Full-Stack
┌─────────────────┐ ┌─────────────────┐
│ Server (SSR) │ → │ Client (SPA) │
│ - Renders HTML │ │ - Hydrates │
│ - Fast initial │ │ - Routes │
│ - SEO ready │ │ - Reactive │
└─────────────────┘ └─────────────────┘
✅ Fast initial load (SSR) ✅ Good SEO (SSR) ✅ Fast subsequent navigation (Client routing) ✅ Progressive enhancement (Reactive state)
🎯 Best Practices
1. Render Critical Content on Server
// ✅ Good: Render above-the-fold content on server
app.get('/products', async (req, res) => {
const products = await db.products.limit(20).findAll();
provideContext('products', products);
res.send(render(ProductsPage()));
});
// ❌ Bad: Empty server response, client-only rendering
app.get('/products', (req, res) => {
res.send('<div id="app"></div>'); // Client will fetch data
});
2. Hydrate Before Adding Interactivity
// ✅ Good: Hydrate first, then enhance
hydrateBySelector('#app', App);
const router = createRouter({ /* ... */ });
// ❌ Bad: Router before hydration
const router = createRouter({ /* ... */ });
hydrateBySelector('#app', App); // Conflicts!
3. Use Reactive State for Client-Only Features
// ✅ Good: Reactive state for UI interactions
import { observable } from '@coherent.js/state';
const sidebarOpen = observable(false); // Client-only UI state
// ✅ Good: SSR state for data
import { withState } from '@coherent.js/core';
const Page = withState({ products: [] })(({ state }) => {
// Rendered on server
});
4. Forms: SSR + Hydration (No Duplication!)
// ✅ Good: Build form once on server, hydrate on client
// SERVER
import { createFormBuilder } from '@coherent.js/forms';
const form = createFormBuilder()
.field('email', { validators: ['email', 'required'] });
const html = render(form.buildForm());
// Renders: <input data-validators="email,required" required />
// CLIENT
import { hydrateForm } from '@coherent.js/forms';
hydrateForm('form'); // Reads data-validators, attaches behavior
// ❌ Bad: Defining form twice (duplication!)
// SERVER: createFormBuilder().field('email', ...)
// CLIENT: createForm({ fields: { email: ... } }) // Why repeat this?
5. Validate on Both Server and Client (Same Rules)
// Shared validators
import { validators } from '@coherent.js/forms';
// Server (REQUIRED for security)
app.post('/api/form', (req, res) => {
const { valid, errors } = validators.validate(req.body, {
email: ['required', 'email']
});
});
// Client (OPTIONAL for UX) - same validators automatically applied via hydration
hydrateForm('form'); // Uses validators from HTML metadata
📝 Forms: Complete SSR + Hydration Example
The Right Way (No Duplication)
// ============================================
// SERVER: Build form with validation metadata
// ============================================
import { createFormBuilder, validators } from '@coherent.js/forms';
app.get('/contact', (req, res) => {
const contactForm = createFormBuilder({ name: 'contact' })
.field('name', {
label: 'Full Name',
required: true,
validators: ['required', 'minLength:3']
})
.field('email', {
label: 'Email',
type: 'email',
required: true,
validators: ['required', 'email']
})
.field('message', {
label: 'Message',
type: 'textarea',
required: true,
validators: ['required', 'minLength:10']
});
// Renders HTML with validation metadata embedded:
// <input name="name"
// data-validators="required,minLength:3"
// required
// data-required="true" />
const html = render({
div: { children: [contactForm.buildForm()] }
});
res.send(wrapHTML(html, '/bundle.js'));
});
// Server-side validation (REQUIRED - never trust client)
app.post('/contact', express.json(), async (req, res) => {
const { valid, errors } = validators.validate(req.body, {
name: ['required', 'minLength:3'],
email: ['required', 'email'],
message: ['required', 'minLength:10']
});
if (!valid) {
return res.status(400).json({ errors });
}
await sendContactEmail(req.body);
res.json({ success: true });
});
// ============================================
// CLIENT: Hydrate server-rendered form
// ============================================
import { hydrateForm } from '@coherent.js/forms';
// Reads validation metadata from HTML and attaches behavior
const contactForm = hydrateForm('form[name="contact"]', {
validateOnBlur: true,
debounce: 300,
async onSubmit(data) {
const response = await fetch('/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const { errors } = await response.json();
// Errors automatically displayed by hydration
return false;
}
alert('Message sent!');
contactForm.reset();
}
});
Key Benefits:
- ✅ No duplication - Form defined once on server
- ✅ Progressive enhancement - Works without JS
- ✅ Shared validation - Same rules on server and client
- ✅ SEO-friendly - Form HTML in initial response
- ✅ Better UX - Client-side validation without full page reload
📦 Package Usage Guide
| Package | Server (SSR) | Client (Hydration) | Client (Enhancement) |
|---|---|---|---|
| @coherent.js/core | ✅ Rendering | ❌ | ❌ |
| @coherent.js/client | ❌ | ✅ Hydration | ✅ Router, HMR |
| @coherent.js/state | ✅ Context API | ❌ | ✅ Reactive state |
| @coherent.js/forms | ✅ Form builder | ✅ Form hydration | ✅ Client validation |
| @coherent.js/express | ✅ Integration | ❌ | ❌ |
🚀 Deployment
Production Build
# Build server bundle
npm run build:server
# Build client bundle
npm run build:client
# Deploy to production
npm run deploy
Environment Variables
# Server
NODE_ENV=production
PORT=3000
# Client bundle path
CLIENT_BUNDLE_URL=/static/bundle.js
🎓 Learning Path
- Start with SSR - Learn
@coherent.js/corerendering - Add Hydration - Make it interactive with
@coherent.js/client - Enable Routing - Add SPA navigation with client router
- Add Reactivity - Enhance UX with
@coherent.js/state - Optimize - Prefetch routes, cache data, monitor performance
📚 Next Steps
- Server-Side Rendering Guide
- Client-Side Hydration
- Client Router
- Reactive State Management
- Full-Stack Forms
Coherent.js gives you the best of both worlds: Fast initial loads with SEO (SSR) + Instant navigation and reactivity (SPA). Build modern full-stack applications with a single framework! 🚀