Boilerplate Overview: Node + React + Fit.js
This boilerplate builds the same extension as Node + React — it lists a company's products — but on the stack Fynd uses for its own Node.js services: Fit.js, with MongoDB and Redis instead of SQLite.
Fit.js is the standard library for conventions shared across Fynd Node.js applications. It provides the server, the named database connections, config validation, structured logging and error reporting, so the extension code only wires them together.
If you do not have the boilerplate, follow Get Started with React + Fit.js Extensions — it covers installing MongoDB and Redis, the Azure DevOps access you need for the fit.js dependency, and generating the extension.
If FDK CLI is already set up, run fdk ext init --template node-react-fit to download the boilerplate explained on this page.
Unlike the SQLite templates, this one stores sessions in MongoDB and Redis. Both must be started before fdk ext preview, or the extension exits while opening connections.
Directory Structure
├── README.md
├── .env # Environment variables (copy from .env.example)
├── .env.example
├── config.js # Config schema — every setting is declared here
├── fdk.ext.config.json # DO NOT TOUCH. Managed by FDK CLI
├── index.js # Entry file: opens connections, then starts the server
├── server.js # Mounts the routers and starts the Fit.js server
├── server
│ ├── connections
│ │ ├── init.js # Boots Redis, MongoDB and Sentry together
│ │ ├── mongo.js # Named MongoDB connection
│ │ ├── redis.js # Named Redis read & write clients
│ │ └── sentry.js
│ ├── fdk
│ │ ├── index.js # setupFdk — auth, callbacks, session storage
│ │ └── proxy.js # Storefront proxy path helpers
│ ├── middleware
│ │ └── error.middleware.js # Logs to Sentry, returns 500
│ ├── router
│ │ └── v1.0
│ │ └── platform.routes.js # Your Platform API routes
│ └── utils
│ └── request.utils.js
├── frontend # Git submodule: example-extension-react
├── test/ # Backend tests
├── jest.config.js
└── package.json
frontend is not a directory in this repository — it is a Git submodule pointing at example-extension-react, the same React app the Node + React, Java and Go templates use. FDK CLI clones it for you during fdk ext init.
Backend
We use the JavaScript Extension Helper Library for authentication, SDK methods and webhooks, and Fit.js for everything around it.
Dependencies
fdk-extension-javascriptfor interacting with Fynd Commerce.fdk-client-javascripta peer dependency for the previous library.fitfor the server, MongoDB and Redis connections, config, logging and Sentry.dotenvandserve-static- Development dependencies like
nodemon,jest,supertest, andaxios-mock-adapter
There is no express or sqlite3 here — Fit.js brings the server, and sessions live in MongoDB and Redis.
fit is hosted in the CommonLibraries project, while the two fdk-* libraries are in JCPLibraries. npm install fails on the fit dependency without read access to both.
Auth Credentials
The FDK CLI configures the API key and secret during development when you run fdk ext preview. The credentials are taken from the Partners panel, saved in the fdk.ext.config.json file, and passed to the backend as environment variables.
Find API credentials: Partner panel → Extensions → <your-extension> → Credentials
Configuration
Unlike the other templates, this one does not read process.env directly in application code. Every setting is declared once in config.js using Fit.js's convict wrapper — with a type, a default and the environment variable it comes from — and then validated on boot.
const convict = require('fit/convict')
const conf = convict({
port: { doc: 'port', format: Number, default: 8080, env: 'PORT' },
extension: {
api_key: { doc: 'extension api key', format: String, default: '', env: 'EXTENSION_API_KEY' },
slug: { doc: 'extension slug', format: String, default: 'invoice-breakdown', env: 'EXTENSION_SLUG' },
},
})
conf.validate({ allowed: 'strict' })
module.exports = conf.get()
Because validation is strict, a missing or mistyped variable fails at startup instead of surfacing as an undefined value later. Add your own settings to this schema and read them off the exported config object.
extension.slug defaults to a placeholder and is used as the prefix for every session key. Set EXTENSION_SLUG to your own extension's slug, otherwise two extensions sharing a Redis instance overwrite each other's sessions.
Environment Variables
Copy .env.example to .env. The connection variables are named after the connection they configure — TEMP_EXAMPLE is the connection name used by the example, so rename both the variables and the names in server/connections/ together.
Set locally:
MONGO_TEMP_EXAMPLE_READ_WRITE,REDIS_TEMP_EXAMPLE_READ_WRITE,REDIS_TEMP_EXAMPLE_READ_ONLY,EXTENSION_SLUG,IS_LOCAL,MODE,SERVER_TYPE
Production values:
EXTENSION_API_KEY,EXTENSION_API_SECRET,EXTENSION_BASE_URL
The following are overridden by FDK CLI during development, avoid setting them yourself:
Avoid:
FRONTEND_PORT,BACKEND_PORT,FP_API_DOMAIN
Connections
index.js is deliberately small: it opens the connections before requiring the server, so no request can arrive before MongoDB and Redis are ready.
initConnections()
.then(() => {
require('./server')
console.log('ℹ️ Backend Server started at:', config.port)
})
.catch((err) => {
logger.error('Error Initializing Connections: ', { err })
})
server/connections/init.js boots all three in parallel.
const { init: redisInit } = require('fit/redis')
const { init: mongoInit } = require('fit/mongo')
const { init: sentryInit } = require('./sentry.js')
module.exports = exports = async () => {
return Promise.all([redisInit(), mongoInit(), sentryInit()])
}
Fit.js reads the MONGO_* and REDIS_* variables and exposes each connection by name on Fit.connections. Redis is split into a write and a read client, so you can point reads at a replica in production.
const { Fit } = require('fit')
module.exports = exports = {
// update redis connection names for your extension
redis: Fit.connections.redis?.temp_example?.write,
redisRead: Fit.connections.redis?.temp_example?.read,
}
Setup FDK
In server/fdk/index.js we call setupFdk with the authentication details, post-auth actions, session storage and access mode. Whenever the seller opens the extension, Fynd Commerce redirects them to the URL returned from the callbacks.auth function.
The important difference from the SQLite templates is MultiLevelStorage: Redis serves reads while MongoDB keeps the session durable, so a cold or flushed cache does not sign every merchant out.
const fdkExtension = setupFdk({
api_key: config.extension.api_key,
api_secret: config.extension.api_secret,
base_url: config.extension.base_url,
cluster: config.cluster,
callbacks: {
auth: async (req) => {
if (req.query.application_id) {
return `${req.extension.base_url}/company/${req.query.company_id}/application/${req.query.application_id}`
} else {
return `${req.extension.base_url}/company/${req.query.company_id}`
}
},
uninstall: async (req) => {
// Write your code here to cleanup data related to extension
},
},
storage: new MultiLevelStorage(config.extension.slug, redis, { mongoose, connection: mongoConnection }),
access_mode: config.extension.access_mode,
})
The auth callback returns two different URLs because an extension can be launched in two contexts: against the whole company, or against a single sales channel. Both paths are matched by the frontend router.
RedisStorage and SQLiteStorage are also exported from fdk-extension-javascript/express/storage — both are shown commented out in the file. MultiLevelStorage is the recommended choice for anything you intend to run in production.
API Mounting
server.js builds the routers. fdkExtension.platformApiRoutes requires a valid session and attaches an authenticated platformClient to the request; fdkExtension.applicationProxyRoutes serves calls proxied from a storefront.
const platformApiRoutes = fdkExtension.platformApiRoutes
const applicationProxyRoutes = fdkExtension.applicationProxyRoutes
platformApiRoutes.use('/', platformRoutes)
applicationProxyRoutes.use('/', appRoutes)
const apiRouter = Server.Router()
apiRouter.use('/platform/v1.0', platformApiRoutes)
apiRouter.use('/application/v1.0', applicationProxyRoutes)
The API is mounted under /api locally, where Vite proxies it to the backend, and at the root in production where Fynd Commerce routes straight to this server.
const mainRouter = Server.Router()
if (config.local) {
mainRouter.use('/api', apiRouter)
} else {
mainRouter.use('/', apiRouter)
}
Finally Server.init receives the routers, the middlewares that run before them — the FDK handler, which owns /fp/* and /adm/*, plus static file serving — and the error handler.
Server.init(
{ main: mainRouter, ui: uiRouter },
[fdkExtension.fdkHandler, serveStatic(STATIC_PATH, { index: false })],
[errorHandler],
{ contentSecurityPolicy: { directives: { frameAncestors: ['*'] } } }
)
Server.start()
The extension is rendered inside an iframe on the Fynd Commerce panel, which is why frameAncestors is opened up. Narrow it to the panel's domain rather than removing it.
API Routing
Routes are defined with Server.Router() in server/router/v1.0/platform.routes.js. Anything mounted under platformApiRoutes has req.platformClient available.
const productRouter = Server.Router()
productRouter.get('/', async function view(req, res, next) {
try {
const { platformClient } = req
const data = await platformClient.catalog.getProducts()
return res.json(data)
} catch (err) {
next(err)
}
})
// Sales-channel products, for a launch scoped to one storefront
productRouter.get('/application/:application_id', async function view(req, res, next) {
try {
const { platformClient } = req
const { application_id } = req.params
const data = await platformClient.application(application_id).catalog.getAppProducts()
return res.json(data)
} catch (err) {
next(err)
}
})
router.use('/products', productRouter)
Passing errors to next(err) is what routes them to the error middleware below.
Error Handling
server/middleware/error.middleware.js is the last middleware. It logs through Fit.js's tracing logger, reports to Sentry, and returns a 500.
exports = module.exports = function errorHandler(err, req, res, next) {
logger.error('Error:', { error: err.stack || err })
Sentry.captureException(err)
return res.status(500).json({ success: false, message: 'Internal Server Error', details: err.stack || err.message })
}
The example returns err.stack in the response body, which is useful while developing and unsafe in production. Drop details before you ship.
Storefront Proxy Path
For a sales-channel launch, the extension has to register a proxy path before the storefront can call it. server/fdk/proxy.js wraps both directions of that.
const addProxyPath = async (companyId, applicationId, platformClient) => {
const partnerAppClient = platformClient.application(applicationId).partner
await partnerAppClient.addProxyPath({
extensionId: config.extension.api_key,
body: { attached_path: config.extension.slug, proxy_url: config.extension.base_url },
})
}
deleteProxyPath is the counterpart — call it from your uninstall callback, because Fynd Commerce does not remove the path for you.
Frontend
The frontend is the shared example-extension-react submodule: React 18 rendered by Vite, identical to the Node + React template. See the Frontend section of that page for the component walkthrough.
Two things are specific to how it talks to this backend:
- The router matches both launch contexts, mirroring the
authcallback.
const router = createBrowserRouter([
{ path: '/company/:company_id/', element: <App /> },
{ path: '/company/:company_id/application/:application_id', element: <App /> },
{ path: '/*', element: <NotFound /> },
])
- During local development Vite proxies the backend paths to the Node process, so the browser only ever talks to the Vite port.
server: {
proxy: {
'^/(\\?.*)?$': proxyOptions,
'^/api(/|(\\?.*)?$)': proxyOptions,
'^/fp(/|(\\?.*)?$)': proxyOptions,
'^/adm(/|(\\?.*)?$)': proxyOptions,
},
}
If you add backend routes outside /api, add a matching proxy rule in frontend/vite.config.js or they will 404 in local development.
The built output goes to frontend/public/dist, which is what serveStatic serves in production — STATIC_PATH switches between that and the submodule root depending on config.local.