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.

The Next.js barcode scanner reading EAN-13 barcodes on a phone

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 isActive prop, with results shown per barcode format and text.

Key Takeaways

  • Use CaptureVisionRouter from dynamsoft-barcode-reader-bundle (v11) — the older dynamsoft-javascript-barcode v9 BarcodeReader/BarcodeScanner APIs are deprecated.
  • Initialize the SDK with LicenseManager.initLicense() and CoreModule.loadWasm(), then decode each camera frame with cvr.capture(enhancer.fetchImage(), 'ReadBarcodes_Default').
  • The SDK is browser-only: it touches window at import time, so a Next.js component that imports it must be loaded with dynamic(() => import(...), { ssr: false }) or the build crashes with window is not defined.
  • getServerSideProps runs on the server at request time, which makes it a clean place to read process.env.DBRLicense and pass the key down as a prop.
  • Point CoreModule.engineResourcePaths.rootDirectory at 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.

  1. 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 container div is the camera view holder, and props.children lets the parent overlay custom UI on top of the video.

  2. Initialize the SDK when the component mounts. Set the resource path to the CDN, activate the license, preload the WebAssembly engine, then create a CaptureVisionRouter and a CameraEnhancer connected to a CameraView:

    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 mounted guard 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. The played event fires once the camera stream starts — that is where the decode loop begins.

  3. Control the camera with the isActive prop: open the camera when it is true, stop the decode loop and close the camera when it is false:

    const toggleCamera = () => {
      if (props.isActive === true) {
        enhancer.current?.open();
      } else {
        stopScanning();
        enhancer.current?.close();
      }
    }
    
    React.useEffect(() => {
      toggleCamera();
    }, [props.isActive]);
    
  4. Decode camera frames on an interval. enhancer.fetchImage() returns the current video frame as a DSImageData object, which cvr.capture() decodes directly. Filter result.items to the barcode items and hand them to onScanned:

    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 decoding flag 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 });
  1. Bind an isActive state to the scanner so a button can start and stop scanning, and an initialized state 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>
        </>
      )
    }
    
  2. Handle the onScanned callback. Each BarcodeResultItem exposes text and formatString, 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.

  1. Add a getServerSideProps function to the index page:

    export async function getServerSideProps() {
      let license: string | undefined = process.env.DBRLicense;
      if (license === undefined) {
        license = "";
      }
      return { props: { license: license } };
    }
    

    getServerSideProps runs on the server at request time, so process.env.DBRLicense is read from the deployment environment — never from the browser.

  2. Pass the license prop to the scanner component:

    <BarcodeScanner
      license={props.license}
      onInitialized={() => setInitialized(true)}
      isActive={isActive}
      onScanned={(results) => onScanned(results)}
    ></BarcodeScanner>
    
  3. 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 defined during next build: the SDK evaluates browser globals at import time. Keep every module that imports dynamsoft-barcode-reader-bundle behind dynamic(() => 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. Set CoreModule.engineResourcePaths = { rootDirectory: "https://cdn.jsdelivr.net/npm/" } before loadWasm() to load them from the CDN, or copy the dist assets into your public folder and point the root directory there.
  • Camera never opens on a plain HTTP origin: getUserMedia only works on HTTPS or localhost. Deployments over HTTPS are required for the live camera; on a LAN test, use python -m http.server plus a tunnel, or just open http://localhost:3000.
  • No results on low-resolution webcams: ReadBarcodes_Default balances speed and coverage. For difficult barcodes, pass template="ReadBarcodes_ReadRateFirst" (or another preset such as ReadBarcodes_Balance, ReadDenseBarcodes, or ReadDistantBarcodes) to the component — the template prop is forwarded to cvr.capture() unchanged.
  • Multiple barcodes in one frame: result.items contains every barcode detected in the frame, so the onScanned callback receives the full list; filter or deduplicate by item.text in your app logic if you only need unique codes.

Source Code

Get the complete sample project source code on GitHub.