Boilerplate Overview: Python (FastAPI) + React
This boilerplate builds the same extension as Node + React — it lists a company's products — on a FastAPI backend with a React frontend and Redis session storage. It is the recommended Python combination; a python-react-sanic template also exists, built the same way on Sanic.
If you do not have the boilerplate, please go through the steps in the Get Started page.
If FDK CLI is already set up, run fdk ext init --template python-react-fastapi to download the boilerplate explained on this page.
You will need Python 3.8 or later and Redis running locally.
Directory Structure
├── README.md
├── .env # Environment variables (copy from .env.example)
├── .env.example
├── fdk.ext.config.json # DO NOT TOUCH. Managed by FDK CLI
├── requirements.txt # Python dependencies
├── main.py # FastAPI app factory: routers, webhooks, static files
├── entrypoint.py
├── app
│ ├── config.py # Pydantic settings read from the environment
│ ├── factory
│ │ ├── boot.py # Startup & shutdown hooks
│ │ └── redis.py # Redis client
│ ├── fdk
│ │ ├── fdk.py # setup_fdk — auth, callbacks, storage
│ │ ├── exntension_handlers.py # auth / install / uninstall callbacks
│ │ └── webhook_handlers.py # Webhook event handlers
│ ├── urls
│ │ ├── application.py # Router definitions
│ │ └── healthz.py
│ └── views
│ ├── products.py # Company products
│ └── app_products.py # Sales-channel products
├── src # React frontend (in this repository, not a submodule)
│ ├── index.js
│ ├── App.jsx
│ ├── router/
│ ├── services/ # Endpoint definitions & axios calls
│ └── views/Home.jsx
├── public/
└── package.json # Frontend dependencies
Unlike the Node, Java and Go templates, this one does not use the shared example-extension-react submodule — the React app is in src/ and is built with react-scripts.
Backend
We use the Python Extension Helper Library to handle authentication, call Platform APIs through the SDK, and process webhooks.
Dependencies
fdk_extension— the FastAPI build of the extension helper library.fdk_client— the Platform API client.fastapiwithuvicornas the server.pydantic[dotenv]for settings,httpxandpython-multipart
Both fdk_* packages are installed straight from Azure DevOps, pinned by tag in requirements.txt:
fdk_extension@git+https://dev.azure.com/GoFynd/JCPLibraries/_git/fdk-extension-python-fastapi@v1.0.0
fdk_client@git+https://dev.azure.com/GoFynd/JCPLibraries/_git/fdk-client-python@v1.10.5-12
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
app/config.py declares the settings as a Pydantic model, which reads them from the environment or .env. EXTENSION_CLUSTER_URL accepts FP_API_DOMAIN as well, which is the variable FDK CLI sets.
class Config(BaseSettings):
REDIS_CONNECTION_HOST: str = "redis://localhost:6379/0"
PORT: int = Field(8080, env=["FRONTEND_PORT", "PORT"])
EXTENSION_API_KEY: str = ""
EXTENSION_API_SECRET: str = ""
EXTENSION_BASE_URL: str = ""
EXTENSION_CLUSTER_URL: str = Field(default="", env=["FP_API_DOMAIN", "EXTENSION_CLUSTER_URL"])
class Config:
env_file = ".env"
CONFIG = Config()
Set locally:
REDIS_CONNECTION_HOST,BASE_URL,ENV
Production values:
EXTENSION_API_KEY,EXTENSION_API_SECRET,EXTENSION_BASE_URL,EXTENSION_CLUSTER_URL
Avoid:
FRONTEND_PORT,PORT,FP_API_DOMAIN— FDK CLI overrides these during development.
Setup FDK
app/fdk/fdk.py calls setup_fdk with the credentials, the callbacks and the session storage. Sessions are kept in Redis under a prefix.
fdk_extension_client = setup_fdk({
"api_key": CONFIG.EXTENSION_API_KEY,
"api_secret": CONFIG.EXTENSION_API_SECRET,
"base_url": CONFIG.EXTENSION_BASE_URL,
"callbacks": {
"auth": extension_handlers.auth,
"uninstall": extension_handlers.uninstall
},
"storage": RedisStorage(redis_client, prefix_key="extension-python-fastapi-react"),
"access_mode": "offline",
"cluster": CONFIG.EXTENSION_CLUSTER_URL,
})
prefix_key namespaces every session key. Set it to your own extension's slug, otherwise two extensions sharing a Redis instance overwrite each other's sessions.
The webhook configuration is present but commented out in the template. Uncomment webhook_config and point the handlers at your own functions to start receiving events.
# "webhook_config": {
# "api_path": "/webhook",
# "notification_email": "test2@abc.com",
# "subscribed_saleschannel": 'specific',
# "event_map": {
# 'company/product/create': {
# "version": '1',
# "handler": webhook_handlers.handle_product_event
# },
# }
# }
Callbacks
app/fdk/exntension_handlers.py holds the callbacks. auth returns the URL the merchant lands on after installing, and covers both launch contexts.
async def auth(self, request: Request):
company_id = int(request.query_params.get("company_id"))
if request.query_params.get("application_id"):
return f"{request.state.extension.base_url}/company/{company_id}/application/{request.query_params.get('application_id')}"
else:
return f"{request.state.extension.base_url}/company/{company_id}"
async def uninstall(self, request: Request):
# Write your code here to cleanup data related to extension
# If task is time taking then process it async on other process.
pass
API Mounting
main.py builds the app. Your routes are appended to the library's platform_api_routes, which is what gives them a session and an authenticated platform client; then both that router and the library's own fdk_route are included.
def create_app() -> FastAPI:
fdk_extension_client: FdkExtensionClient = get_extension_client()
app = FastAPI(lifespan=lifespan)
app.include_router(health_router)
from app.urls.application import app_router
fdk_extension_client.platform_api_routes.append(app_router)
app.include_router(fdk_extension_client.fdk_route)
app.include_router(fdk_extension_client.platform_api_routes.router)
fdk_route carries the install and auth endpoints Fynd Commerce calls — the equivalent of mounting fdkHandler in the JavaScript templates.
The lifespan context runs startup and shutdown from app/factory/boot.py, which is where the Redis connection is closed cleanly.
API Routing
Routers are declared in app/urls/application.py under the /api/v1.0 prefix, with class-based views supplying the handlers.
product_router = APIRouter(prefix="/products")
app_product_router = APIRouter(prefix="/{application_id}/products")
product_router.add_api_route("", ProductData().get, methods=["GET"])
app_product_router.add_api_route("", AppProductData().get, methods=["GET"])
app_router = APIRouter(prefix="/api/v1.0")
app_router.include_router(product_router)
app_router.include_router(app_product_router)
The platform client is on request.state, put there by the library's middleware:
class ProductData:
async def get(self, request: Request):
platform_client: PlatformClient = request.state.platform_client
response = await platform_client.catalog.getProducts()
return JSONResponse(response["json"], status_code=200)
The Python SDK returns a dict with the raw response, so the handler reads response["json"] rather than the object itself.
Webhook Handler
The webhook endpoint is declared inline in main.py. process_webhook verifies the delivery came from Fynd Commerce before dispatching to your handler.
@app.post("/api/v1.0/webhooks")
async def handle_webhook(request: Request):
try:
await fdk_extension_client.webhook_registry.process_webhook(request)
return JSONResponse({"msg": "success"}, status_code=200)
except Exception as e:
return JSONResponse({"msg": f"err: {str(e)}"}, status_code=400)
This route is /api/v1.0/webhooks, while the commented-out webhook_config uses api_path: "/webhook". The path you register with the platform is where deliveries are sent, so set both to the same value when you enable webhooks.
Frontend routes
The launch URLs are client-side routes, so both are served the built index.html and the SPA takes over. The static build is mounted last so it does not shadow the API routes.
@app.get("/company/{company_id}")
async def home_page_handler(request: Request, company_id: str):
return FileResponse(os.path.join(BUILD_DIR, "index.html"), media_type="text/html")
@app.get("/company/{company_id}/application/{application_id}")
async def home_page_handler_app(request: Request, company_id: str, application_id: str):
return FileResponse(os.path.join(BUILD_DIR, "index.html"), media_type="text/html")
if os.path.exists(BUILD_DIR):
app.mount("/", StaticFiles(directory=BUILD_DIR), name="static")
BUILD_DIR is build/, produced by npm run build. Without it the API still serves, but the launch URLs return nothing useful.
Frontend
React rendered by react-scripts, with the API layer split into two files.
Dependencies
reactandreact-domfor rendering the UI.react-router-domfor routing.axiosfor API calls, withurl-joinandwindow-or-global.
Routing
Both launch contexts render <Home />, guarded by a loader:
const router = createBrowserRouter([
{ path: "/company/:company_id/", element: <Home />, loader: routeGuard },
{ path: "/company/:company_id/application/:application_id", element: <Home />, loader: routeGuard },
]);
Calling your backend
Endpoints are declared once in src/services/endpoint.service.js, matching the /api/v1.0 prefix from the backend router:
const Endpoints = {
GET_ALL_PRODUCTS() {
return urlJoin(envVars.EXAMPLE_MAIN_URL, "/api/v1.0/products");
},
GET_ALL_APPLICATION_PRODUCTS(applicationId) {
return urlJoin(envVars.EXAMPLE_MAIN_URL, `/api/v1.0/${applicationId}/products`);
}
};
And src/services/main-service.js wraps them in axios calls. An interceptor attaches the company header to every request, so individual calls do not have to.
axios.interceptors.request.use((config) => {
config.headers["x-company-id"] = getCompany();
return config;
});
The platform routes need that header to resolve the session — without it the request is rejected.