Every time I wanted to build a Chrome Extension, I'd spend the first 2–3 hours doing the exact same thing:
- Fighting Manifest V3 config
- Wiring up TypeScript with @types/chrome
- Figuring out how the popup talks to the background service worker
- Setting up Vite for multi-entry builds
- Creating the options page storage logic
Before writing a single line of my actual idea.
So I stopped doing that. I packaged everything into a boilerplate I can clone and start building immediately — and I'm sharing it here.
What's Included
chrome-extension-boilerplate/
├── manifest.json ← Manifest V3, fully configured
├── vite.config.ts ← Multi-entry build (popup, bg, content, options)
├── tsconfig.json ← Strict TypeScript
└── src/
├── types.ts ← Shared types across all contexts
├── utils/
│ ├── storage.ts ← chrome.storage.sync helpers
│ └── messaging.ts ← Type-safe message passing
├── popup/ ← Dark UI, toggle, live tab URL
├── background/ ← Service worker + message router
├── content/ ← Content script + toast notifications
└── options/ ← Settings page, API key field
The Part That Always Broke Me: Messaging
The trickiest part of Chrome Extensions is getting the three contexts — popup, background service worker, and content script — to talk to each other reliably. Here's how the boilerplate handles it:
Shared types (src/types.ts):
export type MessageType =
| "GET_SETTINGS"
| "SAVE_SETTINGS"
| "PING"
| "GET_PAGE_DATA";
export interface MessageRequest {
type: MessageType;
payload?: unknown;
}
export interface MessageResponse {
success: boolean;
data?: unknown;
error?: string;
}
Sending from popup to background (src/utils/messaging.ts):
export function sendToBackground(request: MessageRequest): Promise<MessageResponse> {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(request, (response: MessageResponse) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
return;
}
resolve(response);
});
});
}
Handling it in the background worker:
chrome.runtime.onMessage.addListener(
(request: MessageRequest, _sender, sendResponse) => {
handleMessage(request).then(sendResponse);
return true; // Keep channel open for async response
}
);
That return true at the end? Took me an embarrassing amount of time to figure out the first time. It keeps the message channel open for async responses — without it, everything silently fails.
Getting Started in 5 Minutes
# 1. Download and extract the boilerplate
# 2. Install dependencies
npm install
# 3. Start dev mode (auto-rebuilds on save)
npm run dev
# 4. Load in Chrome
# → chrome://extensions → Developer Mode ON
# → Load unpacked → select the /dist folder
Your extension is live. Now just edit the source files and it rebuilds automatically.
Adding AI / OpenAI Integration
One of the most common things people add to Chrome Extensions right now is AI. Here's all you need:
npm install openai
Then in your background service worker:
import OpenAI from "openai";
import { loadSettings } from "../utils/storage";
async function runAI(prompt: string): Promise<string> {
const settings = await loadSettings();
const client = new OpenAI({ apiKey: settings.apiKey });
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: prompt }],
});
return response.choices[0].message.content ?? "";
}
The options page already has an API key field wired to chrome.storage.sync — so users can enter their key and it's securely stored locally.
Publishing to the Chrome Web Store
Once you're ready to ship:
npm run build
Zip the dist/ folder, head to the Chrome Web Store Developer Dashboard, pay the one-time $5 fee, upload, and submit. Review takes 1–3 days.
Get the Boilerplate
The full source is available here — grab it, build something, ship it.
MIT licensed. Use it in unlimited personal and commercial projects.
If this saves you even one hour of setup time, it's worth it. And if you build something cool with it, drop a link in the comments — I'd love to see what you make.
Top comments (0)