How to Build a Barcode and QR Code Scanner in Next.js with SSR
You can build a barcode and QR code scanner in Next.js with the dynamsoft-barcode-reader-bundle package by wrapping the SDK in a React component and loading that component with next/dynamic and ssr: false. The scanner decodes live camera frames through CaptureVisionRouter.capture(), while getServerSideProps reads the license key from a server environment variable so the key never has to be hard-coded in client source.

What You’ll Build
- A reusable
<BarcodeScanner />React component that opens the camera, scans video frames, and reports decoded barcodes through props callbacks. - A Next.js page that pre-renders on the server (
getServerSideProps) to inject the license key, then renders the scanner only in the browser. - Start/Stop scanning control driven by an
isActiveprop, with results shown per barcode format and text.
Key Takeaways
- Use
CaptureVisionRouterfromdynamsoft-barcode-reader-bundle(v11) — the olderdynamsoft-javascript-barcodev9BarcodeReader/BarcodeScannerAPIs are deprecated. - Initialize the SDK with
LicenseManager.initLicense()andCoreModule.loadWasm(), then decode each camera frame withcvr.capture(enhancer.fetchImage(), 'ReadBarcodes_Default'). - The SDK is browser-only: it touches
windowat import time, so a Next.js component that imports it must be loaded withdynamic(() => import(...), { ssr: false })or the build crashes withwindow is not defined. getServerSidePropsruns on the server at request time, which makes it a clean place to readprocess.env.DBRLicenseand pass the key down as a prop.- Point
CoreModule.engineResourcePaths.rootDirectoryat the jsDelivr CDN so the WebAssembly engine, workers, and model files load without bundling ~50 MB of binaries into your app.
Prerequisites
- Node.js 16+ and a Next.js project (Pages Router).
- A Dynamsoft license to run beyond the 24-hour public trial — get a 30-day free trial license for Dynamsoft Barcode Reader.
- A camera-capable browser over HTTPS or
localhost(camera access requires a secure context).
Common Developer Questions
Which npm package should I install for barcode scanning in Next.js?
Install dynamsoft-barcode-reader-bundle. It bundles the barcode reader, camera enhancer, and WebAssembly engine in one package, so you do not need separate dynamsoft-javascript-barcode or dynamsoft-camera-enhancer installs.
How do I migrate from dynamsoft-javascript-barcode v9 to the bundle?
Replace BarcodeReader.license = "..." with LicenseManager.initLicense("..."), replace BarcodeReader.createInstance() + reader.decode(frame) with CaptureVisionRouter.createInstance() + cvr.capture(frame, templateName), and read results from result.items (filtering item.type === EnumCapturedResultItemType.CRIT_BARCODE) instead of TextResult.barcodeText.
Why does importing the SDK crash my Next.js build?
The SDK accesses window as soon as the module loads. During the server-render phase there is no window, so Next.js fails with window is not defined. Load the component that imports the SDK with next/dynamic and ssr: false so it is only ever evaluated in the browser.
Can I still use server-side rendering for the license key?
Yes. getServerSideProps runs on the server, reads process.env.DBRLicense, and passes the value as a page prop. The scanner component itself stays client-only, so you get server-side license injection without SSR crashes.
Step 1: Create the Next.js Project
Create a new Next.js project named barcode-scanner:
npx create-next-app@latest barcode-scanner
Then run the development server to verify the scaffold:
cd barcode-scanner
npm run dev
Step 2: Install the Barcode Reader Bundle
Install the Dynamsoft Barcode Reader bundle, which includes the camera enhancer and the WebAssembly engine:
npm install dynamsoft-barcode-reader-bundle
Step 3: Create the Barcode Scanner React Component
Create src/components/BarcodeScanner.tsx. The component opens the camera, reads frames on an interval, and decodes barcodes with the Capture Vision router.
-
Import the SDK classes and define the component props:
import React from "react"; import { ReactNode } from "react"; import { CameraEnhancer, CameraView, CaptureVisionRouter, CoreModule, EnumCapturedResultItemType, LicenseManager, } from "dynamsoft-barcode-reader-bundle"; import type { BarcodeResultItem, PlayCallbackInfo, } from "dynamsoft-barcode-reader-bundle"; const DEFAULT_LICENSE = "<license>"; export interface ScannerProps { isActive?: boolean; children?: ReactNode; interval?: number; license?: string; template?: string; onInitialized?: (enhancer: CameraEnhancer, cvr: CaptureVisionRouter) => void; onScanned?: (results: BarcodeResultItem[]) => void; onPlayed?: (playCallbackInfo: PlayCallbackInfo) => void; onClosed?: () => void; } const BarcodeScanner = (props: ScannerProps): React.ReactElement => { const container = React.useRef<HTMLDivElement>(null); const enhancer = React.useRef<CameraEnhancer>(); const cvr = React.useRef<CaptureVisionRouter>(); return ( <div ref={container} style={{ position: "relative", width: "100%", height: "100%" }}> {props.children} </div> ); } export default BarcodeScanner;The
containerdiv is the camera view holder, andprops.childrenlets the parent overlay custom UI on top of the video. -
Initialize the SDK when the component mounts. Set the resource path to the CDN, activate the license, preload the WebAssembly engine, then create a
CaptureVisionRouterand aCameraEnhancerconnected to aCameraView:const mounted = React.useRef(false); React.useEffect(() => { const init = async () => { CoreModule.engineResourcePaths = { rootDirectory: "https://cdn.jsdelivr.net/npm/", }; await LicenseManager.initLicense( props.license && props.license.length > 0 ? props.license : DEFAULT_LICENSE ); await CoreModule.loadWasm(); const cvRouter = await CaptureVisionRouter.createInstance(); cvr.current = cvRouter; const cameraView = await CameraView.createInstance(); const cameraEnhancer = await CameraEnhancer.createInstance(cameraView); enhancer.current = cameraEnhancer; cameraEnhancer.setVideoFit("cover"); await cameraEnhancer.setUIElement(container.current!); cameraEnhancer.on("played", (playCallbackInfo: PlayCallbackInfo) => { if (props.onPlayed) { props.onPlayed(playCallbackInfo); } startScanning(); }); cameraEnhancer.on("cameraClose", () => { if (props.onClosed) { props.onClosed(); } }); if (props.onInitialized) { props.onInitialized(cameraEnhancer, cvRouter); } toggleCamera(); }; if (mounted.current === false) { init(); } mounted.current = true; }, []);The
mountedguard prevents double initialization when React Strict Mode runs effects twice in development.setUIElement(container.current)injects the SDK’s video panel (camera selector, resolution selector, and<video>element) into the component’s own DOM node, so the camera UI lives inside the React tree. Theplayedevent fires once the camera stream starts — that is where the decode loop begins. -
Control the camera with the
isActiveprop: open the camera when it istrue, stop the decode loop and close the camera when it isfalse:const toggleCamera = () => { if (props.isActive === true) { enhancer.current?.open(); } else { stopScanning(); enhancer.current?.close(); } } React.useEffect(() => { toggleCamera(); }, [props.isActive]); -
Decode camera frames on an interval.
enhancer.fetchImage()returns the current video frame as aDSImageDataobject, whichcvr.capture()decodes directly. Filterresult.itemsto the barcode items and hand them toonScanned:const interval = React.useRef<any>(null); const decoding = React.useRef(false); const startScanning = () => { const decode = async () => { if (decoding.current === false && cvr.current && enhancer.current) { decoding.current = true; const result = await cvr.current.capture( enhancer.current.fetchImage(), props.template ? props.template : "ReadBarcodes_Default" ); const items = result.items.filter( (item) => item.type === EnumCapturedResultItemType.CRIT_BARCODE ) as BarcodeResultItem[]; if (props.onScanned) { props.onScanned(items); } decoding.current = false; } } interval.current = setInterval(decode, props.interval ? props.interval : 40); } const stopScanning = () => { clearInterval(interval.current); }The
decodingflag prevents frames from piling up when a decode takes longer than the interval.
Step 4: Use the Barcode Scanner Component in the App
Switch to src/pages/index.tsx. The scanner component is browser-only, so load it through next/dynamic with ssr: false:
import dynamic from 'next/dynamic'
import type { BarcodeResultItem } from 'dynamsoft-barcode-reader-bundle'
import Head from 'next/head'
import React from 'react';
import homeStyles from '../styles/Home.module.css';
// The barcode scanner is browser-only, so load it without server-side rendering.
const BarcodeScanner = dynamic(() => import('@/components/BarcodeScanner'), { ssr: false });
-
Bind an
isActivestate to the scanner so a button can start and stop scanning, and aninitializedstate so the button only appears after the SDK is ready:export default function Home(props: any) { const [isActive, setIsActive] = React.useState(false); const [initialized, setInitialized] = React.useState(false); const toggleScanning = () => { setIsActive(!isActive); } return ( <> <Head> <title>Next.js Barcode Reader</title> <meta name="description" content="Generated by create next app" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/favicon.ico" /> </Head> <main> <div className={homeStyles.app}> <h2>Next.js Barcode Scanner</h2> {initialized ? ( <button onClick={toggleScanning}>{isActive ? "Stop Scanning" : "Start Scanning"}</button> ) : ( <div>Initializing...</div> )} <div className={homeStyles.barcodeScanner}> <BarcodeScanner isActive={isActive} ></BarcodeScanner> </div> </div> </main> </> ) } -
Handle the
onScannedcallback. EachBarcodeResultItemexposestextandformatString, so the page lists every detected barcode and stops the scan:const onScanned = (results: BarcodeResultItem[]) => { if (results.length > 0) { let text = ""; for (let index = 0; index < results.length; index++) { const result = results[index]; text = text + (result.formatString || "Unknown") + ": " + result.text + "\n"; } alert(text); setIsActive(false); } }<BarcodeScanner onInitialized={() => setInitialized(true)} isActive={isActive} onScanned={(results) => onScanned(results)} ></BarcodeScanner>
Step 5: Read the License from Environment Variables
Hard-coding a license key in the component works but leaks the key into the client bundle. With Next.js server-side rendering, you can read the key from a server environment variable and pass it down as a prop instead.
-
Add a
getServerSidePropsfunction to the index page:export async function getServerSideProps() { let license: string | undefined = process.env.DBRLicense; if (license === undefined) { license = ""; } return { props: { license: license } }; }getServerSidePropsruns on the server at request time, soprocess.env.DBRLicenseis read from the deployment environment — never from the browser. -
Pass the license prop to the scanner component:
<BarcodeScanner license={props.license} onInitialized={() => setInitialized(true)} isActive={isActive} onScanned={(results) => onScanned(results)} ></BarcodeScanner> -
Set the environment variable before starting the server:
# Windows set DBRLicense=<your license key> # Linux / macOS export DBRLicense=<your license key>
That completes the barcode and QR code scanner in Next.js. Check the online demo to try it — point the camera at any 1D or 2D barcode and the decoded text appears.
Common Issues & Edge Cases
window is not definedduringnext build: the SDK evaluates browser globals at import time. Keep every module that importsdynamsoft-barcode-reader-bundlebehinddynamic(() => import(...), { ssr: false }). Importing only types from the package (import type { BarcodeResultItem } ...) is safe in server code because type imports are erased at compile time.- WASM or worker 404s after bundling: when webpack bundles the SDK, the engine can lose track of its sibling
.wasm,.worker.js, and model files. SetCoreModule.engineResourcePaths = { rootDirectory: "https://cdn.jsdelivr.net/npm/" }beforeloadWasm()to load them from the CDN, or copy thedistassets into yourpublicfolder and point the root directory there. - Camera never opens on a plain HTTP origin:
getUserMediaonly works on HTTPS orlocalhost. Deployments over HTTPS are required for the live camera; on a LAN test, usepython -m http.serverplus a tunnel, or just openhttp://localhost:3000. - No results on low-resolution webcams:
ReadBarcodes_Defaultbalances speed and coverage. For difficult barcodes, passtemplate="ReadBarcodes_ReadRateFirst"(or another preset such asReadBarcodes_Balance,ReadDenseBarcodes, orReadDistantBarcodes) to the component — thetemplateprop is forwarded tocvr.capture()unchanged. - Multiple barcodes in one frame:
result.itemscontains every barcode detected in the frame, so theonScannedcallback receives the full list; filter or deduplicate byitem.textin your app logic if you only need unique codes.
Source Code
Get the complete sample project source code on GitHub.