GraphQL for Frontend Developers: Queries, Mutations, and Caching
A practical guide to using GraphQL in frontend applications — writing queries and mutations, client-side caching with Apollo and urql, and patterns for React integration.
A practical guide to building Progressive Web Apps — service workers for offline support, web app manifest for installation, caching strategies, and turning your website into an installable app.
By codefunc
Progressive Web Apps bridge the gap between websites and native apps. They load like regular web pages but offer offline support, push notifications, and home screen installation. PWAs work on any device with a modern browser — no app store required.
Bottom line: A PWA needs three things: HTTPS, a web app manifest (build one with the Web Manifest Builder), and a service worker. Pair icons with the Favicon Generator. The manifest enables installation. The service worker enables offline support and caching. Start with a caching strategy that fits your content: network-first for dynamic data, cache-first for static assets.
| Feature | Implementation |
|---|---|
| Installable | Web app manifest |
| Offline capable | Service worker with caching |
| Secure | HTTPS required |
| Responsive | CSS media queries |
| App-like | Full-screen mode, navigation |
| Fresh | Service worker updates |
The manifest tells browsers how to install and display your app.
{
"name": "My Awesome App",
"short_name": "MyApp",
"description": "An awesome progressive web app",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2563eb",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/screenshots/home.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "/screenshots/mobile.png",
"sizes": "750x1334",
"type": "image/png",
"form_factor": "narrow"
}
]
}
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#2563eb">
<link rel="apple-touch-icon" href="/icons/icon-192.png">
| Mode | Description |
|---|---|
fullscreen |
No browser UI, entire screen |
standalone |
App-like, no URL bar |
minimal-ui |
Minimal browser controls |
browser |
Standard browser tab |
Service workers intercept network requests, enabling offline support and caching.
// sw.js
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html',
];
// Install - cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
// Activate - clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
});
// Fetch - serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => response || fetch(event.request))
.catch(() => caches.match('/offline.html'))
);
});
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW registered:', registration.scope);
})
.catch((error) => {
console.log('SW registration failed:', error);
});
});
}
Best for: CSS, JS, images, fonts that rarely change.
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((cached) => {
if (cached) {
return cached;
}
return fetch(event.request).then((response) => {
// Cache new resources
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clone);
});
return response;
});
})
);
});
Best for: API responses, user-specific data.
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// Cache fresh response
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clone);
});
return response;
})
.catch(() => caches.match(event.request))
);
});
Best for: Content that should be fresh but can show stale briefly.
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((cached) => {
const fetched = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return cached || fetched;
});
})
);
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// API requests: network-first
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
return;
}
// Static assets: cache-first
if (url.pathname.match(/\.(css|js|png|jpg|svg|woff2)$/)) {
event.respondWith(cacheFirst(event.request));
return;
}
// HTML pages: network-first with offline fallback
event.respondWith(
fetch(event.request)
.catch(() => caches.match('/offline.html'))
);
});
Workbox is a library that simplifies service worker development.
npm install workbox-webpack-plugin
// vite.config.js
import { VitePWA } from 'vite-plugin-pwa';
export default {
plugins: [
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.example\.com\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 60 * 60 * 24, // 1 day
},
},
},
],
},
manifest: {
name: 'My App',
short_name: 'MyApp',
theme_color: '#2563eb',
icons: [
{
src: '/icon-192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/icon-512.png',
sizes: '512x512',
type: 'image/png',
},
],
},
}),
],
};
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// Images: cache-first
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
// API: network-first
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-responses',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 minutes
}),
],
})
);
// CSS/JS: stale-while-revalidate
registerRoute(
({ request }) =>
request.destination === 'style' ||
request.destination === 'script',
new StaleWhileRevalidate({
cacheName: 'static-resources',
})
);
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent default prompt
e.preventDefault();
deferredPrompt = e;
// Show your custom install button
document.getElementById('install-button').style.display = 'block';
});
document.getElementById('install-button').addEventListener('click', async () => {
if (!deferredPrompt) return;
// Show native prompt
deferredPrompt.prompt();
// Wait for user response
const { outcome } = await deferredPrompt.userChoice;
console.log(`User response: ${outcome}`);
deferredPrompt = null;
document.getElementById('install-button').style.display = 'none';
});
// Detect if already installed
window.addEventListener('appinstalled', () => {
console.log('PWA installed');
deferredPrompt = null;
});
// Check display mode
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
const isFromHomeScreen = window.navigator.standalone; // iOS
if (isStandalone || isFromHomeScreen) {
console.log('Running as installed PWA');
}
// Detect online/offline
window.addEventListener('online', () => {
document.body.classList.remove('offline');
});
window.addEventListener('offline', () => {
document.body.classList.add('offline');
});
// Initial check
if (!navigator.onLine) {
document.body.classList.add('offline');
}
.offline-indicator {
display: none;
position: fixed;
bottom: 0;
width: 100%;
padding: 0.5rem;
background: #fef3c7;
text-align: center;
}
.offline .offline-indicator {
display: block;
}
// Save action for later
async function saveForSync(action) {
const queue = JSON.parse(localStorage.getItem('syncQueue') || '[]');
queue.push(action);
localStorage.setItem('syncQueue', JSON.stringify(queue));
}
// Process queue when online
window.addEventListener('online', async () => {
const queue = JSON.parse(localStorage.getItem('syncQueue') || '[]');
for (const action of queue) {
try {
await fetch(action.url, action.options);
} catch (error) {
console.error('Sync failed:', error);
return; // Stop on failure
}
}
localStorage.setItem('syncQueue', '[]');
});
// Register sync (in main app)
async function savePost(post) {
// Save to IndexedDB
await saveToIndexedDB('posts', post);
// Register sync if offline
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-posts');
}
}
// Handle sync (in service worker)
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-posts') {
event.waitUntil(syncPosts());
}
});
async function syncPosts() {
const posts = await getFromIndexedDB('posts');
for (const post of posts) {
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(post),
});
await deleteFromIndexedDB('posts', post.id);
}
}
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
// Send subscription to server
await fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
});
}
}
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
event.waitUntil(
self.registration.showNotification(data.title || 'Notification', {
body: data.body,
icon: '/icon-192.png',
badge: '/badge.png',
data: { url: data.url },
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url || '/')
);
});
// Detect update available
navigator.serviceWorker.ready.then((registration) => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New version available
showUpdateBanner();
}
});
});
});
function showUpdateBanner() {
const banner = document.createElement('div');
banner.innerHTML = `
<p>New version available!</p>
<button onclick="updateApp()">Update</button>
`;
document.body.appendChild(banner);
}
function updateApp() {
navigator.serviceWorker.ready.then((registration) => {
registration.waiting?.postMessage({ type: 'SKIP_WAITING' });
});
window.location.reload();
}
// Service worker - handle skip waiting
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
DevTools → Lighthouse → Check "Progressive Web App"
DevTools → Application:
DevTools → Network → Check "Offline"
Use these codefunc tools alongside this guide:
Start simple: manifest for installation, basic service worker for offline. Cache static assets with cache-first, dynamic content with network-first. Use Workbox to simplify service worker code. Test with Lighthouse and in offline mode. Add push notifications only if they provide real user value. Remember that PWAs enhance progressively — they still work without service worker support.
A practical guide to using GraphQL in frontend applications — writing queries and mutations, client-side caching with Apollo and urql, and patterns for React integration.
Why compact GraphQL queries hurt reviews, how to format selection sets consistently, and a browser workflow before you paste into Apollo or GraphiQL.
Shrink SVG icons for the web — strip comments and whitespace, choose inline vs data URLs, and know when you still need a full SVGO pipeline.
Find a developer tool