The fundamental challenge of using CSpell with Next.js stems from the framework's architecture, which separates code into two distinct environments: the server (Node.js) and the client (browser). CSpell is a Node.js-based library that needs direct access to the file system to read its dictionary files. This is a capability the browser simply does not have.

This is why, by default, attempts to use CSpell in a Next.js application result in errors like "Module not found: Can't resolve 'fs'." The browser cannot find or access the Node.js fs module, which is essential for loading the dictionaries.

The Server-Side Solution

The most robust way to integrate CSpell is to run it exclusively on the server side. In a Next.js application, this means placing your CSpell logic inside an API Route or a Server Component. These are special files that are guaranteed to execute only on the server, giving them access to the Node.js file system and other back-end resources.

By creating a utility class like the SpellChecker shown below, you can centralize all your spell-checking logic. This class handles the asynchronous dictionary loading and provides a clean API for checking text, ensuring that the heavy lifting is always done on the server.

The Path Solution: Ensuring Dictionaries are Found

A common pitfall is that even on the server, CSpell can fail to locate the dictionary files. This often happens in deployment environments where the file structure is different from your local development machine. The solution is to provide explicit, absolute paths to the dictionary files.

The provided code uses a helper function, getDictionaryDefinitions, which uses path.join(process.cwd(), 'node_modules/...') to construct a reliable file path.

process.cwd() returns the current working directory, which is the root of your Next.js project.

path.join(...)safely concatenates the paths, creating an absolute path that will work consistently across different environments.

This ensures that the spell checker always knows where to find the dictionaries, making your application reliable and portable.

Here is the complete SpellChecker class:

// lib/spell-checker.ts
import { spellCheckDocument, Document, SpellCheckFileOptions, CSpellUserSettings } from "cspell-lib";
import path from "path";
/**
 * Creates and returns the SpellCheckerSettings object with explicit paths to dictionaries.
 * This is a more robust approach for Next.js environments.
 * @param locale The language locale to use.
 * @returns The SpellCheckerSettings object.
 */
function getDictionaryDefinitions(locale: string): CSpellUserSettings {
    const commonSettings: CSpellUserSettings = {
        words: [],
        suggestionsTimeout: 10000,
        dictionaryDefinitions: [],
    };

    if (locale === 'ar') {
        // You would need to install this package for the path to exist
        // npm install @cspell/dict-ar
        commonSettings.dictionaries = ['ar'];
        commonSettings.dictionaryDefinitions = [
            {
                name: 'ar',
                path: path.join(process.cwd(), 'node_modules/@cspell/dict-ar/ar.trie.gz'),
            }
        ];
    } else {
        // Default to English if locale is not 'ar'
        commonSettings.dictionaries = ['en_us', 'en-gb', 'en-us-common-misspellings', 'en-gb-common-misspellings'];
        commonSettings.dictionaryDefinitions = [
            {
                name: 'en_us',
                path: path.join(process.cwd(), 'node_modules/@cspell/dict-en_us/en_US.trie.gz'),
            },
            {
                name: 'en-gb',
                path: path.join(process.cwd(), 'node_modules/@cspell/dict-en_gb/en_GB.trie.gz'),
            },
            {
                name: 'en-us-common-misspellings',
                path: path.join(process.cwd(), 'node_modules/@cspell/dict-en-us-common-misspellings/dist/dict-en-us.json'),
            },
            {
                name: 'en-gb-common-misspellings',
                path: path.join(process.cwd(), 'node_modules/@cspell/dict-en-gb-common-misspellings/dist/dict-en-gb.json'),
            }
        ];
    }

    return commonSettings;
}

/**
 * The SpellChecker class for performing spell checking.
 * It uses a static factory method to handle asynchronous dictionary loading.
 */
export class SpellChecker {
    private locale = "en";
    /**
     * An async factory method to create and initialize a SpellChecker instance.
     * With cspell-lib's 'spellCheckDocument' function, we don't need to load
     * dictionaries into a stateful instance. This method simply maintains the
     * requested class-based API.
     * @returns A promise that resolves to a SpellChecker instance.
     */
    public static async create(locale: string): Promise<SpellChecker> {
        return new SpellChecker(locale);
    }

    // The constructor is private to enforce using the async factory method.
    private constructor(locale = "en") {
        this.locale = locale;
    }

    /**
     * Checks if a single word is spelled correctly.
     * @param word The word to check.
     * @returns A promise that resolves to true if the word is correct, otherwise false.
     */
    public async isCorrect(word: string): Promise<boolean> {
        const doc: Document = {
            uri: "text.txt",
            text: word,
            languageId: "plaintext",
            locale: this.locale,
        };

        const checkOptions: CheckOptions = {
            generateSuggestions: false,
            noConfigSearch: true,
        };

        const settings: CSpellUserSettings = {
            ...getDictionaryDefinitions(this.locale),
        };

        const result = await spellCheckDocument(doc, checkOptions, settings);

        return result.issues.length === 0;
    }

    /**
     * Gets spelling suggestions for a misspelled word.
     * @param word The misspelled word.
     * @returns A promise that resolves to an array of suggested correct spellings.
     */
    public async suggest(word: string): Promise<string[]> {
        const doc: Document = {
            uri: "text.txt",
            text: word,
            languageId: "plaintext",
            locale: this.locale,
        };

        const checkOptions: CheckOptions = {
            generateSuggestions: true,
            noConfigSearch: true,
        };

        const settings: CSpellUserSettings = {
            ...getDictionaryDefinitions(this.locale),
            suggestionsTimeout: 2000,
        };

        const result = await spellCheckDocument(doc, checkOptions, settings);

        if (result.issues.length > 0) {
            return result.issues[0].suggestions || [];
        }

        return [];
    }

    /**
     * Checks an entire text for spelling errors.
     * @param text The text to check.
     * @returns A promise that resolves to an array of objects,
     * each containing a misspelled word and its suggestions.
     */
    public async checkText(text: string): Promise<{ word: string; suggestions: string[] }[]> {
        const doc: Document = {
            uri: "text.txt",
            text: text,
            languageId: "plaintext",
            locale: this.locale,
        };

        const checkOptions: SpellCheckFileOptions = {
            generateSuggestions: true,
            noConfigSearch: true,
        };

        const settings: CSpellUserSettings = {
            ...getDictionaryDefinitions(this.locale),
            suggestionsTimeout: 10000,
            numSuggestions: 5,
            ignoreRegExpList: [/\bhttps?:\/\/\S+\b/g, /\bwww\.\S+\b/g, /\b\d+\b/g, /[#@]\w+/g],
        };

        const errors: { word: string; suggestions: string[] }[] = [];

        const result = await spellCheckDocument(doc, checkOptions, settings);

        for (const issue of result.issues) {
            errors.push({
                word: issue.text,
                suggestions: issue.suggestions || [],
            });
        }
        return errors;
    }
}