Skip to main content

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.

Prerequisite

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
The frontend lives in this repository

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.
  • fastapi with uvicorn as the server.
  • pydantic[dotenv] for settings, httpx and python-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,
})
Change the storage prefix

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)
note

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)
Keep the paths in sync

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 the frontend first

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

  • react and react-dom for rendering the UI.
  • react-router-dom for routing.
  • axios for API calls, with url-join and window-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.


Was this section helpful?