Skip to main content
Advertisement

19.2 Environment Setup

This chapter covers setting up a Qwik development environment and getting your first project running. QwikCity is Qwik's official meta-framework, providing routing, SSR, API routes, and more.


Prerequisites

# Node.js 18+ required (20 LTS recommended)
node --version # v20.x.x or higher

# npm, pnpm, and yarn are all supported
npm --version # 9.x or higher recommended

Creating a QwikCity Project

Basic Creation Command

npm create qwik@latest

Running this starts an interactive prompt:

? Where would you like to create your new project? › ./my-qwik-app

? Select a starter › (use arrow keys)
❯ Empty App (Qwik City) ← recommended: blank app
Library ← for building a library
Qwik Todo App ← example app

? Would you like to install npm dependencies? › (Y/n)
❯ Yes
No

? Initialize a new git repository? › (Y/n)
❯ Yes
No

Quick Creation (no prompts)

# Create an Empty App in a specific directory
npm create qwik@latest -- --it empty my-qwik-app

# Then
cd my-qwik-app
npm start

Running the Initial Project

cd my-qwik-app
npm install # install packages (skip if already done)
npm start # start dev server (localhost:5173)

Project Structure

Directory structure of the generated project:

my-qwik-app/
├── public/ # static assets (images, favicon, etc.)
│ └── favicon.svg
├── src/
│ ├── components/ # reusable components
│ │ └── router-head/
│ │ └── router-head.tsx
│ ├── routes/ # file-based routing (QwikCity core)
│ │ ├── index.tsx # / route (home page)
│ │ ├── layout.tsx # root layout
│ │ └── service-worker.ts # service worker (prefetching)
│ ├── entry.dev.tsx # dev server entry point
│ ├── entry.preview.tsx # preview entry point
│ ├── entry.ssr.tsx # SSR entry point (server)
│ └── global.css # global styles
├── .eslintrc.cjs # ESLint configuration
├── .prettierrc # Prettier configuration
├── package.json
├── tsconfig.json # TypeScript configuration
└── vite.config.ts # Vite + Qwik Optimizer configuration

Key Files Overview

src/routes/index.tsx — home page component:

import { component$ } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';

export default component$(() => {
return (
<>
<h1>Hello Qwik!</h1>
<p>Edit src/routes/index.tsx to get started.</p>
</>
);
});

export const head: DocumentHead = {
title: 'Qwik App',
meta: [
{
name: 'description',
content: 'Qwik site description',
},
],
};

src/routes/layout.tsx — root layout:

import { component$, Slot } from '@builder.io/qwik';
import { routerHead } from '~/components/router-head/router-head';

export default component$(() => {
return (
<>
<RouterHead /> {/* manages <head> tags */}
<body>
<header>
<nav>Navigation</nav>
</header>
<main>
<Slot /> {/* child routes render here */}
</main>
<footer>Footer</footer>
</body>
</>
);
});

vite.config.ts — Vite + Qwik Optimizer:

import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig(() => {
return {
plugins: [
qwikCity(), // QwikCity plugin (routing, SSR)
qwikVite(), // Qwik Optimizer ($ transforms, chunk splitting)
tsconfigPaths(), // tsconfig path support
],
preview: {
headers: {
'Cache-Control': 'public, max-age=600',
},
},
};
});

Qwik Optimizer — Vite Plugin and $ Transformation

What the Optimizer Does

Developer code → Qwik Optimizer → optimized code

1. Transforms $ functions into QRLs (Qwik URL References)
2. Splits closures into separate chunk files
3. Establishes server/client code boundaries
4. Validates serializability and emits warnings

Optimizer in Action

// Developer writes:
export const App = component$(() => {
const count = useSignal(0);
const greet = $(() => `Hello! count: ${count.value}`);

return (
<div>
<button onClick$={() => count.value++}>Increment</button>
<p onClick$={greet}>Message</p>
</div>
);
});

// After Optimizer transformation (conceptually):
// [chunk-app.js]
export const App = componentQrl(
qrl(() => import('./chunk-app_component.js'), 'App_component')
);

// [chunk-app_component.js]
export const App_component = () => {
const count = useSignal(0);
const greet = qrl(
() => import('./chunk-app_greet.js'), 'App_greet',
[count] // captured variables
);
return ...;
};

// [chunk-app_onClick.js]
export const App_onClick = (count) => { count.value++; };

// [chunk-app_greet.js]
export const App_greet = (count) => `Hello! count: ${count.value}`;

Customizing the Optimizer

// vite.config.ts
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';

export default defineConfig(() => {
return {
plugins: [
qwikVite({
// specify build targets
client: {
outDir: './dist/client',
},
ssr: {
outDir: './dist/server',
},
// debug mode (print $ transformation steps)
debug: process.env.NODE_ENV === 'development',
}),
],
};
});

Dev Server and HMR

Dev Server Commands

# Start dev server (default port: 5173)
npm start
# or
npm run dev

# Specify a custom port
npm start -- --port 3000

# Allow external access (for testing on other devices)
npm start -- --host

How HMR (Hot Module Replacement) Works

Qwik's HMR is more sophisticated than standard Vite HMR:

File change detected

Only the changed component is recompiled

Updated chunk sent to browser

UI updates while preserving state (Signal-based)
// Code pattern that works well with HMR
// src/components/my-component.tsx
import { component$, useSignal } from '@builder.io/qwik';

export const MyComponent = component$(() => {
const count = useSignal(0); // state preserved across HMR

// JSX changes are reflected immediately via HMR
return (
<div class="my-component">
<h2>Changes reflect instantly</h2>
<button onClick$={() => count.value++}>
Clicks: {count.value}
</button>
</div>
);
});

Dev Server Configuration

// vite.config.ts
export default defineConfig(() => {
return {
plugins: [...],
server: {
port: 3000,
open: true, // auto-open browser
host: true, // allow external access
cors: true, // enable CORS
},
// environment variable prefix
envPrefix: ['VITE_', 'PUBLIC_'],
};
});

TypeScript Setup

tsconfig.json

{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"jsxImportSource": "@builder.io/qwik",
"strict": true,
"noEmit": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"paths": {
"~/*": ["./src/*"]
},
"types": ["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}

Core Qwik TypeScript Types

import type {
QwikIntrinsicElements, // JSX element types
QRL, // Qwik Reference Link type
Signal, // type for useSignal
NoSerialize, // mark as non-serializable
Component, // return type of component$
PropFunction, // $ function passed as a component prop
DocumentHead, // head metadata type
} from '@builder.io/qwik';

// Defining component Props types
interface ButtonProps {
label: string;
disabled?: boolean;
// use PropFunction when receiving a $ function as a prop
onClick$?: PropFunction<() => void>;
// or use the QRL type directly
onHover$?: QRL<(event: MouseEvent) => void>;
}

export const Button = component$<ButtonProps>(({
label,
disabled = false,
onClick$
}) => {
return (
<button
disabled={disabled}
onClick$={onClick$}
>
{label}
</button>
);
});

Using Signal Types

import { component$, useSignal, useStore } from '@builder.io/qwik';
import type { Signal } from '@builder.io/qwik';

// useSignal returns Signal<T>
const count: Signal<number> = useSignal(0);
const name: Signal<string> = useSignal('Alice');
const user: Signal<User | null> = useSignal(null);

// useStore returns a reactive object of type T
interface AppState {
count: number;
items: string[];
isLoading: boolean;
}

const state: AppState = useStore<AppState>({
count: 0,
items: [],
isLoading: false,
});

Qwik VS Code Extensions and Settings

Required VS Code Extensions

// .vscode/extensions.json
{
"recommendations": [
"builder.qwik-vscode", // Official Qwik extension
"dbaeumer.vscode-eslint", // ESLint
"esbenp.prettier-vscode", // Prettier
"bradlc.vscode-tailwindcss", // Tailwind CSS (if used)
"ms-vscode.vscode-typescript-next", // Latest TypeScript
"unifiedjs.vscode-mdx" // MDX support (optional)
]
}

Qwik VS Code Extension Features

Features provided by the qwik-vscode extension:
1. Autocomplete and type hints for $ functions
2. QRL reference tracking (F12 navigation)
3. Component snippets
4. Serialization warning highlighting
5. IntelliSense for useSignal/useStore

Optimizing VS Code Settings

// .vscode/settings.json
{
// TypeScript
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.tsdk": "./node_modules/typescript/lib",

// Auto-format on save
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},

// ESLint
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"typescript",
"typescriptreact"
],

// File associations
"files.associations": {
"*.tsx": "typescriptreact"
},

// Tailwind CSS IntelliSense (if used)
"tailwindCSS.experimental.classRegex": [
["class\\$\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
]
}

Useful Code Snippets

// .vscode/qwik.code-snippets
{
"Qwik Component": {
"prefix": "qcomp",
"body": [
"import { component$ } from '@builder.io/qwik';",
"",
"export const ${1:ComponentName} = component$(() => {",
" return (",
" <div>",
" ${2:content}",
" </div>",
" );",
"});"
],
"description": "Basic Qwik component structure"
},
"Qwik Signal": {
"prefix": "qsig",
"body": "const ${1:name} = useSignal(${2:initialValue});",
"description": "Create a useSignal"
},
"Qwik Store": {
"prefix": "qstore",
"body": [
"const ${1:state} = useStore({",
" ${2:key}: ${3:value},",
"});"
],
"description": "Create a useStore"
},
"Qwik Route": {
"prefix": "qroute",
"body": [
"import { component$ } from '@builder.io/qwik';",
"import type { DocumentHead } from '@builder.io/qwik-city';",
"",
"export default component$(() => {",
" return (",
" <main>",
" <h1>${1:Page Title}</h1>",
" </main>",
" );",
"});",
"",
"export const head: DocumentHead = {",
" title: '${1:Page Title}',",
"};"
],
"description": "QwikCity route component"
}
}

ESLint + Prettier Setup for Qwik

ESLint Configuration

# Install ESLint packages (usually included when creating a project)
npm install -D eslint eslint-plugin-qwik @typescript-eslint/parser @typescript-eslint/eslint-plugin
// .eslintrc.cjs
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
ecmaVersion: 2021,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:qwik/recommended', // Qwik-specific rules
],
rules: {
// Qwik-specific rules
'qwik/no-use-after-await': 'error', // no hooks after await
'qwik/use-method-usage': 'error', // restrict hook call location
'qwik/valid-lexical-scope': 'error', // serialization validity
'qwik/jsx-no-script-url': 'error',

// TypeScript rules
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],

// General rules
'no-console': 'warn',
'prefer-const': 'error',
},
};

Prettier Configuration

// .prettierrc
{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"bracketSpacing": true,
"arrowParens": "always",
"jsxSingleQuote": false,
"endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
}
// .prettierignore
node_modules
dist
.qwik
build
coverage
*.min.js

lint-staged + husky Setup (pre-commit checks)

npm install -D husky lint-staged
npx husky init
// add to package.json
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,css}": [
"prettier --write"
]
}
}
# .husky/pre-commit
npx lint-staged

Building and Previewing

Build Commands

# Production build
npm run build

# Build process:
# 1. TypeScript compilation and type checking
# 2. Qwik Optimizer runs ($ transforms, chunk splitting)
# 3. Vite bundling
# 4. SSR bundle generation (for server)
# 5. Client bundle generation (for browser)
# 6. Service worker bundle generation

Build Output Structure

dist/
├── build/ # client-side chunks
│ ├── q-*.js # auto-generated Qwik chunks (hashed)
│ ├── qwikloader.js # ~1KB loader
│ └── ...
├── client/ # SSG static files
│ ├── index.html
│ └── ...
└── server/
└── entry.express.js # SSR server entry point

Preview

# Serve build output locally
npm run preview
# → http://localhost:4173

# Build and preview in one step
npm run build && npm run preview

Bundle Analysis and Optimization

// Add bundle analysis to vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig(() => {
return {
plugins: [
qwikCity(),
qwikVite(),
tsconfigPaths(),
// bundle analysis (generates stats.html after build)
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
filename: 'dist/stats.html',
}),
],
build: {
// chunk optimization
rollupOptions: {
output: {
manualChunks: {
// separate shared libraries
vendor: ['@builder.io/qwik', '@builder.io/qwik-city'],
},
},
},
},
};
});

Environment Variables

.env File Structure

# .env (shared across all environments)
PUBLIC_API_URL=https://api.example.com
PUBLIC_SITE_NAME=My Qwik App

# .env.local (local only, included in .gitignore)
PRIVATE_DB_URL=postgresql://localhost:5432/mydb
PRIVATE_JWT_SECRET=super-secret-key

# .env.production (production only)
PUBLIC_API_URL=https://api.mysite.com

Using Environment Variables

// Using PUBLIC_ variables on the client
import { component$ } from '@builder.io/qwik';

export const MyComponent = component$(() => {
// PUBLIC_ prefix variables are exposed to the client
const apiUrl = import.meta.env.PUBLIC_API_URL;
const siteName = import.meta.env.PUBLIC_SITE_NAME;

return <p>API: {apiUrl} | Site: {siteName}</p>;
});

// Using PRIVATE_ variables on the server
import { routeLoader$ } from '@builder.io/qwik-city';

export const useData = routeLoader$(async () => {
// runs server-side only, so private variables are accessible
const dbUrl = process.env.PRIVATE_DB_URL;
// DB queries, etc.
});

TypeScript Type Declarations

// src/env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly PUBLIC_API_URL: string;
readonly PUBLIC_SITE_NAME: string;
// declare additional environment variables as needed
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}

Adapter Configuration (per deployment target)

QwikCity provides adapters for various platforms:

# Cloudflare Pages adapter
npm run qwik add cloudflare-pages

# Vercel Edge Functions adapter
npm run qwik add vercel-edge

# Node.js Express adapter
npm run qwik add express

# Azure Static Web Apps
npm run qwik add azure-swa

# AWS Lambda
npm run qwik add aws-lambda

Cloudflare Pages Adapter Setup

// vite.config.ts (after adding the Cloudflare adapter)
import { defineConfig } from 'vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';
import { cloudflarePagesAdapter } from '@builder.io/qwik-city/adapters/cloudflare-pages/vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig(() => {
return {
plugins: [
qwikCity(),
qwikVite(),
tsconfigPaths(),
cloudflarePagesAdapter(), // Cloudflare adapter
],
};
});

Pro Tips

Tip 1: Using Path Aliases

// tsconfig.json
{
"compilerOptions": {
"paths": {
"~/*": ["./src/*"], // ~ → src/
"@components/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"]
}
}
}

// Usage example
import { Button } from '~/components/button/button';
import { formatDate } from '@utils/date';

Tip 2: Development Debug Configuration

// src/entry.dev.tsx
import { render } from '@builder.io/qwik';
import { Router } from '@builder.io/qwik-city';
import Root from './root';

// Enable Qwik debug mode in development
if (import.meta.env.DEV) {
// enable serialization warnings
(globalThis as any).QWIK_DEVTOOLS = true;
}

render(document, <Root />);

Tip 3: Build Validation for CI/CD

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- run: npm ci
- run: npm run lint # ESLint check
- run: npm run build # build validation
- run: npm run test # tests (if any)

Tip 4: Setting Up i18n with Qwik Speak

# Internationalization library
npm install qwik-speak
// src/speak-config.ts
import type { SpeakConfig } from 'qwik-speak';

export const config: SpeakConfig = {
defaultLocale: { lang: 'en', currency: 'USD', timeZone: 'America/New_York' },
supportedLocales: [
{ lang: 'en', currency: 'USD', timeZone: 'America/New_York' },
{ lang: 'ko', currency: 'KRW', timeZone: 'Asia/Seoul' },
],
assets: ['app'], // translation file name
};
Recommended structure for production projects:
src/
├── components/
│ ├── ui/ # general-purpose UI components (Button, Input, etc.)
│ ├── layout/ # layout components
│ └── features/ # feature-specific components
├── routes/
│ ├── (auth)/ # auth-related route group
│ ├── (app)/ # app route group
│ └── api/ # API routes
├── services/ # API service functions
├── utils/ # utility functions
├── types/ # shared type definitions
└── hooks/ # custom hooks (use...)

Summary

CommandDescription
npm create qwik@latestCreate a new QwikCity project
npm startStart dev server (port 5173)
npm run buildProduction build
npm run previewPreview build output
npm run qwik add <adapter>Add a deployment adapter
npm run lintRun ESLint
Config filePurpose
vite.config.tsVite + Qwik Optimizer configuration
tsconfig.jsonTypeScript config (jsxImportSource is critical)
.eslintrc.cjsESLint + Qwik rules
.prettierrcCode formatting
src/routes/Root directory for file-based routing
src/entry.ssr.tsxSSR entry point

With the environment configured, the next chapter dives deep into Qwik's core concepts: Signal, Store, and the various hooks.

Advertisement