--- title: Guard Platform-Specific APIs in Universal SSR Code impact: HIGH impactDescription: Accessing browser-only APIs on server causes crashes; Node.js APIs fail in browser type: gotcha tags: [vue3, ssr, browser-api, nodejs, universal, isomorphic, server-side-rendering] --- # Guard Platform-Specific APIs in Universal SSR Code **Impact: HIGH** - SSR applications run the same code on both server (Node.js) and client (browser). Browser APIs like `window`, `document`, and `localStorage` don't exist in Node.js and will throw `ReferenceError`. Similarly, Node.js APIs like `fs` and `process` aren't available in browsers. Universal/isomorphic code must guard platform-specific API access or use libraries that work on both platforms. ## Task Checklist - [ ] Never access `window`, `document`, `navigator` in `setup()` or `created()` - [ ] Move browser API access to `onMounted()` lifecycle hook - [ ] Use `typeof window !== 'undefined'` guard when needed outside lifecycle - [ ] Use cross-platform libraries for common functionality (fetch, storage) - [ ] Use Nuxt's `process.client` / `process.server` guards in Nuxt projects ## Common Browser APIs That Break SSR | API | Node.js Behavior | |-----|-----------------| | `window` | `ReferenceError: window is not defined` | | `document` | `ReferenceError: document is not defined` | | `localStorage` / `sessionStorage` | `ReferenceError` | | `navigator` | `ReferenceError` | | `location` | `ReferenceError` | | `history` | `ReferenceError` | | `alert` / `confirm` / `prompt` | `ReferenceError` | | `requestAnimationFrame` | `ReferenceError` | | `IntersectionObserver` | `ReferenceError` | | `ResizeObserver` | `ReferenceError` | **Incorrect - Crashes on Server:** ```javascript // WRONG: These run during setup/SSR - crashes in Node.js const width = ref(window.innerWidth) const theme = localStorage.getItem('theme') const userAgent = navigator.userAgent ``` ```vue ``` **Correct - Use onMounted:** ```vue ``` **Correct - Guard with typeof:** ```javascript // When you need to check outside lifecycle hooks function getStoredValue(key, defaultValue) { if (typeof window !== 'undefined' && window.localStorage) { return localStorage.getItem(key) ?? defaultValue } return defaultValue } // Composable with SSR awareness export function useMediaQuery(query) { const matches = ref(false) // Only run on client if (typeof window !== 'undefined') { const mediaQuery = window.matchMedia(query) matches.value = mediaQuery.matches // Setup listener in lifecycle onMounted(() => { const handler = (e) => { matches.value = e.matches } mediaQuery.addEventListener('change', handler) onUnmounted(() => mediaQuery.removeEventListener('change', handler)) }) } return matches } ``` ## Nuxt.js Guards ```vue ``` ```vue ``` ## Cross-Platform Libraries Use libraries that abstract platform differences: ```javascript // Fetch - works in both Node.js 18+ and browsers const response = await fetch('/api/data') // For older Node.js, use node-fetch or axios import axios from 'axios' const { data } = await axios.get('/api/data') ``` ```javascript // Universal cookie handling import Cookies from 'js-cookie' // Client only import { parse } from 'cookie' // Works both // In Nuxt, use useCookie() const token = useCookie('auth-token') ``` ## Common Node.js APIs That Break in Browser | API | Browser Behavior | |-----|-----------------| | `fs` | Module not found | | `path` | Module not found | | `process` (full) | Undefined or limited | | `Buffer` | Undefined (unless polyfilled) | | `__dirname` / `__filename` | Undefined | | `require()` | Undefined in ES modules | **Incorrect:** ```javascript // WRONG: Node.js APIs in universal code import fs from 'fs' const config = JSON.parse(fs.readFileSync('./config.json')) ``` **Correct - Separate Server Code:** ```javascript // server/utils.js - Server-only file import fs from 'fs' export function loadConfig() { return JSON.parse(fs.readFileSync('./config.json')) } // app.js - Universal code uses API instead const config = await fetch('/api/config').then(r => r.json()) ``` ## Environment Detection Utility ```javascript // utils/environment.js export const isClient = typeof window !== 'undefined' export const isServer = !isClient export const isBrowser = isClient && typeof document !== 'undefined' export const isNode = typeof process !== 'undefined' && process.versions?.node != null // Usage import { isClient, isServer } from '@/utils/environment' if (isClient) { // Browser-specific code } ``` ## Third-Party Library Issues Some libraries auto-access browser APIs on import: ```javascript // WRONG: Library accesses window on import import SomeChartLibrary from 'some-chart-library' // ^ Crashes on server if library does: const x = window.something ``` **Correct - Dynamic Import:** ```vue ``` ## Reference - [Vue.js SSR - Platform-Specific APIs](https://vuejs.org/guide/scaling-up/ssr.html#access-to-platform-specific-apis) - [Nuxt ClientOnly Component](https://nuxt.com/docs/api/components/client-only) - [MDN: Web APIs](https://developer.mozilla.org/en-US/docs/Web/API)