Code Patterns
On this page 17
Real-world patterns for building stx applications. These are the recommended approaches -- follow them consistently.
Auth Flow Pattern
A shared auth store with token management, guarded pages, and authenticated fetch.
Store: stores/auth.ts
defineStore('auth', () => {
const user = state(null)
const token = state(localStorage.getItem('auth_token') || '')
const isAuthenticated = derived(() => !!token())
function login(data: any, authToken: string) {
user.set(data)
token.set(authToken)
localStorage.setItem('auth_token', authToken)
}
function logout() {
user.set(null)
token.set('')
localStorage.removeItem('auth_token')
navigate('/login', true)
}
async function authFetch(url: string, options: RequestInit = {}) {
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token()}`,
},
})
}
return { user, token, isAuthenticated, login, logout, authFetch }
})
Page: pages/dashboard.stx
<scriptclient
const auth = useStore('auth')
const data = state(null)
onMount(async () => {
if (!auth.isAuthenticated()) return navigate('/login', true)
data.set(await auth.authFetch('/api/dashboard').then(r => r.json()))
})
</script>
<divif="">Loading dashboard</div>
<divif="">
<h1text=""></h1>
<ptext=""></p>
</div>
Page: pages/login.stx
<scriptclient
const auth = useStore('auth')
const email = state('')
const password = state('')
const error = state('')
const loading = state(false)
async function handleLogin() {
loading.set(true)
error.set('')
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email(), password: password() }),
})
if (!res.ok) throw new Error('Invalid credentials')
const { user, token } = await res.json()
auth.login(user, token)
navigate('/dashboard', true)
} catch (e) {
error.set(e.message)
}
loading.set(false)
}
</script>
<formsubmitprevent="">
<divif=""class=""text=""></div>
<inputtype=""x-model=""placeholder=""class="" />
<inputtype=""x-model=""placeholder=""class="" />
<buttontype=""disabled="">
<spanif="">Signing in</span>
<spanif="">Sign In</span>
</button>
</form>
Data Fetching with Loading State
The standard pattern for fetching data with loading, error, and empty states.
<scriptclient
const loading = state(true)
const items = state([])
const error = state('')
onMount(async () => {
try {
const res = await fetch('/api/items')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
items.set(await res.json())
} catch (e) {
error.set(e.message)
}
loading.set(false)
})
</script>
<divif=""class="">
<spanclass=""></span> Loading...
</div>
<divif=""class="">
<ptext=""></p>
<buttonclick="">Retry</button>
</div>
<divif=""class="">
No items found.
</div>
<divfor=""key=""class="">
<h3text=""></h3>
<ptext=""></p>
</div>
Form with Validation
Client-side form handling with field-level validation and submission.
<scriptclient
const form = state({ name: '', email: '', message: '' })
const errors = state({})
const submitting = state(false)
const submitted = state(false)
function validate() {
const e = {}
const f = form()
if (!f.name.trim()) e.name = 'Name is required'
if (!f.email.includes('@')) e.email = 'Valid email is required'
if (f.message.length < 10) e.message = 'Message must be at least 10 characters'
errors.set(e)
return Object.keys(e).length === 0
}
function updateField(field, value) {
form.set({ ...form(), [field]: value })
}
async function handleSubmit() {
if (!validate()) return
submitting.set(true)
try {
await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form()),
})
submitted.set(true)
} catch (e) {
errors.set({ form: e.message })
}
submitting.set(false)
}
</script>
<divif=""class="">
Thanks for your message!
</div>
<formif=""submitprevent="">
<divif=""class=""text=""></div>
<divclass="">
<div>
<label>Name</label>
<inputtype=""value=""input=""class="" />
<spanif=""class=""text=""></span>
</div>
<div>
<label>Email</label>
<inputtype=""value=""input=""class="" />
<spanif=""class=""text=""></span>
</div>
<div>
<label>Message</label>
<textareavalue=""input=""class=""></textarea>
<spanif=""class=""text=""></span>
</div>
<buttontype=""disabled=""class="">
<spanif="">Sending</span>
<spanif="">Send Message</span>
</button>
</div>
</form>
Composable Extraction
When a component grows past ~80 lines of script, extract logic into composables.
Before: Everything inline (bad)
<scriptclient
const items = state([])
const loading = state(true)
const error = state('')
const search = state('')
const sortBy = state('name')
const sortDir = state('asc')
const page = state(1)
const perPage = 20
const filtered = derived(() => {
let result = items()
if (search()) {
result = result.filter(i => i.name.toLowerCase().includes(search().toLowerCase()))
}
result.sort((a, b) => {
const dir = sortDir() === 'asc' ? 1 : -1
return a[sortBy()] > b[sortBy()] ? dir : -dir
})
return result
})
const paginated = derived(() => {
const start = (page() - 1) * perPage
return filtered().slice(start, start + perPage)
})
const totalPages = derived(() => Math.ceil(filtered().length / perPage))
onMount(async () => {
try {
items.set(await fetch('/api/items').then(r => r.json()))
} catch (e) { error.set(e.message) }
loading.set(false)
})
// ... 100 more lines
</script>
After: Composable in functions/
functions/useFilteredList.ts
export function useFilteredList(fetchUrl: string, perPage = 20) {
const items = state([])
const loading = state(true)
const error = state('')
const search = state('')
const sortBy = state('name')
const sortDir = state('asc')
const page = state(1)
const filtered = derived(() => {
let result = items()
if (search()) {
result = result.filter(i =>
i.name.toLowerCase().includes(search().toLowerCase())
)
}
result.sort((a, b) => {
const dir = sortDir() === 'asc' ? 1 : -1
return a[sortBy()] > b[sortBy()] ? dir : -dir
})
return result
})
const paginated = derived(() => {
const start = (page() - 1) * perPage
return filtered().slice(start, start + perPage)
})
const totalPages = derived(() => Math.ceil(filtered().length / perPage))
async function load() {
try {
items.set(await fetch(fetchUrl).then(r => r.json()))
} catch (e) {
error.set(e.message)
}
loading.set(false)
}
return { items, loading, error, search, sortBy, sortDir, page, filtered, paginated, totalPages, load }
}
Page (clean):
<scriptclient
import { useFilteredList } from '@/functions/useFilteredList'
const { loading, error, search, paginated, totalPages, page, load } = useFilteredList('/api/items')
onMount(() => load())
</script>
<inputtype=""x-model=""placeholder=""class="" />
<divif="">Loading</div>
<divfor=""key="">
<spantext=""></span>
</div>
<divclass="">
<buttonclick=""disabled="">Prev</button>
<spantext=""></span>
<buttonclick=""disabled="">Next</button>
</div>
Responsive Images
Use <StxImage> for optimized image rendering with responsive sizes.
<StxImage
src="/images/hero.jpg"
alt="Hero banner"
width="1200"
height="600"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 1200px"
loading="lazy"
class="w-full rounded-lg"
/>
For a gallery with multiple images:
<divclass="">
<divfor=""key="">
<StxImage
:src="photo.url"
:alt="photo.title"
width="400"
height="300"
loading="lazy"
class="rounded shadow"
/>
</div>
</div>
Middleware Guard
Protect routes with middleware that runs before page rendering.
middleware/auth.ts
export default function authMiddleware({ request, redirect }) {
const token = request.headers.get('cookie')?.match(/auth_token=([^;]+)/)?.[1]
if (!token) {
return redirect('/login')
}
}
Protected page: pages/settings.stx
<scriptserver
export const meta = {
middleware: ['auth'],
}
</script>
<scriptclient
const auth = useStore('auth')
const settings = state(null)
onMount(async () => {
settings.set(await auth.authFetch('/api/settings').then(r => r.json()))
})
</script>
<h1>Account Settings</h1>
<divif="">
<!-- settings form -->
</div>
SPA Navigation with Layout Transitions
Use <StxLink> for SPA navigation. The router handles layout transitions automatically -- even between pages with different layouts, no page reload occurs.
Layout: layouts/app.stx
<html>
<head><title>My App</title></head>
<bodyclass="">
<navclass="">
<StxLinkto=""activeClass="">Home</StxLink>
<StxLinkto=""activeClass="">Dashboard</StxLink>
<StxLinkto=""activeClass="">Login</StxLink>
</nav>
<mainclass="">
@slot
</main>
</body>
</html>
Layout: layouts/auth.stx
<html>
<head><title>Sign In</title></head>
<bodyclass="">
<divclass="">
@slot
</div>
</body>
</html>
Navigating between layouts
<!-- From a page using layouts/app.stx -->
<StxLinkto="">Sign In</StxLink>
<!-- From a page using layouts/auth.stx -->
<StxLinkto="">Back to Dashboard</StxLink>
The router detects that these pages use different layout groups (app vs auth) and performs a full body swap instead of a fragment swap. Both transitions happen client-side -- no page reload.
Key distinction: <StxLink> does SPA navigation. A plain <a href> always triggers a full page reload. Use <StxLink> for in-app links, <a href> for external URLs.