Boilerplate Overview: Java + React
This boilerplate builds the same extension as Node + React — it lists a company's products — on a Spring Boot backend with the shared React frontend. It is the recommended Java combination; a java-vue template also exists, built the same way with a Vue 2 frontend.
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 java-react to download the boilerplate explained on this page.
You will also need Java 17 or later and Maven installed.
Directory Structure
├── README.md
├── pom.xml # Maven dependencies
├── mvnw, mvnw.cmd # Maven wrapper
├── fdk.ext.config.json # DO NOT TOUCH. Managed by FDK CLI
├── src
│ ├── main
│ │ ├── java/com/fynd/example/java
│ │ │ ├── ExampleJavaApplication.java # Entry point, extension bean & callbacks
│ │ │ ├── controller
│ │ │ │ ├── PlatformController.java # Your Platform API routes
│ │ │ │ ├── RouteController.java # Forwards launch URLs to the frontend
│ │ │ │ └── WebhookController.java # Webhook delivery endpoint
│ │ │ └── webhook
│ │ │ └── SampleEventHandler.java # A webhook event handler
│ │ └── resources
│ │ ├── application.yml # Extension, webhook & server config
│ │ └── application-prod.yml
│ └── test/ # Backend tests
└── frontend # Git submodule: example-extension-react
frontend is a Git submodule pointing at example-extension-react — the same React app the Node templates use. FDK CLI clones it during fdk ext init.
Backend
We use the Java Extension Helper Library to handle authentication, call Platform APIs through the SDK, and subscribe to webhooks.
Dependencies
fdk-extension-javafor interacting with Fynd Commerce.fdk-client-javaa peer dependency for the previous library.spring-boot-starter-webfor the server, withspring-boot-devtoolsfor reload during development.lombokfor the@Slf4jlogger and boilerplate reduction.- Test dependencies:
spring-boot-starter-test,mockito-inline,junit
Both libraries are published to the com.azure.gofynd group; their versions are pinned as properties in pom.xml.
<fdk-extension.version>0.8.1</fdk-extension.version>
<fdk-client.version>3.1.0-beta.6</fdk-client.version>
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
Everything the extension needs is declared in src/main/resources/application.yml and bound to the library's ExtensionProperties. The values come from environment variables, which FDK CLI sets during development.
server:
port: ${BACKEND_PORT:8080}
ext:
api_key: ${EXTENSION_API_KEY}
api_secret: ${EXTENSION_API_SECRET}
scopes: ""
base_url: ${EXTENSION_BASE_URL}
cluster: ${FP_API_DOMAIN:https://api.fynd.com}
webhook:
api_path: "/api/webhook-events"
notification_email: "useremail@example.com"
event_map:
- name: "product/delete"
handler: sampleHandler
category: "company"
version: 1
Sessions are stored in SQLite, configured at the top of the same file:
sqlite:
db:
url: 'jdbc:sqlite:session_storage.db'
notification_email is where Fynd Commerce writes when a webhook delivery keeps failing. Point it at an address your team actually reads before going live.
Setup FDK
ExampleJavaApplication.java is both the Spring Boot entry point and where the extension is initialised. The Extension bean receives the properties, the session storage, and the callbacks.
@Bean
public com.fynd.extension.model.Extension getExtension() throws ClassNotFoundException {
Extension extension = new Extension();
return extension.initialize(
extensionProperties,
new SQLiteStorage(dbUrl, REDIS_KEY),
callbacks
);
}
REDIS_KEY is the prefix every session key is stored under — set it to your own extension's slug so two extensions never collide.
The callbacks are supplied as an ExtensionCallback with four handlers: auth, install, uninstall and auto-install. The auth handler returns the URL the merchant lands on after installing, and returns two different paths depending on the launch context.
ExtensionCallback callbacks = new ExtensionCallback((request) -> {
Session fdkSession = (Session) request.getAttribute("session");
if (request.getParameter("application_id") != null) {
return extensionProperties.getBaseUrl() + "/company/" + fdkSession.getCompanyId()
+ "/application/" + request.getParameter("application_id");
} else {
return extensionProperties.getBaseUrl() + "/company/" + fdkSession.getCompanyId();
}
}, (context) -> {
logger.info("In install callback");
return extensionProperties.getBaseUrl();
}, (fdkSession) -> {
logger.info("In uninstall callback"); // clean up your data for this company here
return extensionProperties.getBaseUrl();
}, (fdkSession) -> {
logger.info("In auto-install callback");
return extensionProperties.getBaseUrl();
});
The component scan has to cover the library packages as well as your own, or the extension's own routes and filters are never registered.
@SpringBootApplication
@ComponentScan(basePackages = {"com.fynd.**", "com.sdk.**"})
public class ExampleJavaApplication { ... }
API Routing
Extend BasePlatformController and every route in the controller runs behind a valid session, with an authenticated PlatformClient on the request.
@RestController
@RequestMapping("/api")
public class PlatformController extends BasePlatformController {
@GetMapping(value = "/products", produces = "application/json")
public CatalogPlatformModels.ProductListingResponseV2 getProducts(HttpServletRequest request) {
PlatformClient platformClient = (PlatformClient) request.getAttribute("platformClient");
return platformClient.catalog.getProducts(/* … */);
}
// Sales-channel products, for a launch scoped to one storefront
@GetMapping(value = "/{application_id}/products", produces = "application/json")
public CatalogPlatformModels.RawProductListingResponse getAppProducts(
@PathVariable("application_id") String applicationId, HttpServletRequest request) {
PlatformClient platformClient = (PlatformClient) request.getAttribute("platformClient");
return platformClient.application(applicationId).catalog.getAppProducts(/* … */);
}
}
The Java SDK takes every query parameter as a positional argument, so calls look longer than their JavaScript equivalents. Pass Collections.emptyList() and "" for the filters you do not need.
Webhook Handler
Two pieces are needed. First a controller on the api_path from application.yml, which hands the request to WebhookService — that is what verifies the delivery really came from Fynd Commerce.
@PostMapping(path = "/api/webhook-events")
public ResponseEntity<Object> receiveWebhookEvents(HttpServletRequest httpServletRequest) {
try {
webhookService.processWebhook(httpServletRequest);
return new ResponseEntity<>(Collections.singletonMap("success", true), HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(Collections.singletonMap("success", false), HttpStatus.BAD_REQUEST);
}
}
Second, a handler bean whose name matches the handler key in the event map.
@Service("sampleHandler")
public class SampleEventHandler implements EventHandler {
@Override
public void handle(String eventName, Object body, String companyId, String applicationId) {
log.info("Event Received : {} for companyId : {}", eventName, companyId);
// code to handle webhook event here
}
}
One handler can serve many events — list it under each entry in event_map.
Frontend routes
The launch URLs are client-side routes, so a hard refresh on them has to return the React app rather than a 404. RouteController forwards both to the root, where the static frontend is served.
@Controller
public class RouteController {
@RequestMapping("/company/{company_id}")
public String redirect() { return "forward:/"; }
@RequestMapping("/company/{company_id}/application/{application_id}")
public String redirectToAppHome() { return "forward:/"; }
}
Frontend
The frontend is the shared example-extension-react submodule — React 18 rendered by Vite, the same app used by the Node templates. See the Frontend section of the Node + React page for the component walkthrough.
Note that it calls /api/products and /api/products/application/:application_id, while this backend exposes the sales-channel route as /api/{application_id}/products. Align the two before the sales-channel launch works.