Custom Route Code Generation Specification
Overview
NUXP’s codegen generates C++ and TypeScript for SDK suite wrappers from parsed Adobe Illustrator SDK headers. But plugin developers also need custom higher-level endpoints that compose multiple SDK calls – for example, “create a planting bed” which involves GetSelectedArt, GetArtBounds, SetNote, and SetArtName in sequence. These don’t map 1:1 to any single SDK function.
Without codegen, custom endpoints are maintained by hand in both C++ and TypeScript, which leads to parameter name mismatches (mode vs lod), field name divergence (perimeter_pts vs perimeter_m), and silent runtime bugs that only surface during integration testing.
Custom route codegen solves this by adding a third input to the pipeline – routes.json – alongside SDK headers and events.json. A CustomRouteGenerator reads this spec and produces three files:
| Generated File | Purpose |
|---|---|
CustomRouteRegistration.cpp |
HttpServer::Get/Post/Delete route wiring |
CustomRouteHandlers.h |
Handler function declarations (developer implements these) |
customRoutes.ts |
Typed TypeScript client functions |
The developer only writes handler implementations. Route paths, parameter names, response types, and HTTP methods all come from the spec.
Configuration: routes.json
Location: codegen/src/config/routes.json
{
"extends": "./base-routes.json",
"namespace": "MyPluginHandlers",
"routes": [
{
"name": "createBed",
"description": "Creates a bed from the current selection",
"method": "POST",
"path": "/beds/create",
"request": {
"bedId": { "type": "string", "description": "Unique bed identifier" },
"bedName": { "type": "string", "description": "Display name", "optional": true }
},
"response": {
"success": { "type": "boolean", "description": "Whether creation succeeded" },
"bedId": { "type": "string", "description": "The created bed's UUID" },
"area_sqm": { "type": "number", "description": "Bed area in square meters" }
}
},
{
"name": "deleteBed",
"description": "Deletes a bed by its ID",
"method": "DELETE",
"path": "/beds/{id}",
"pathParams": {
"id": { "type": "string", "pattern": "[a-zA-Z0-9_-]+", "description": "Bed identifier" }
},
"response": {
"success": { "type": "boolean" },
"deleted": { "type": "number" }
}
},
{
"name": "getDocInfo",
"description": "Returns document information",
"method": "GET",
"path": "/doc/info",
"response": {
"name": { "type": "string" },
"path": { "type": "string" },
"artboardCount": { "type": "number" }
}
}
]
}
Top-Level Fields
| Field | Type | Required | Description |
|---|---|---|---|
extends |
string |
No | Path to a base routes.json to inherit routes from |
namespace |
string |
No | C++ namespace for handler declarations (default: "CustomHandlers") |
routes |
Route[] |
Yes | Array of route definitions |
Route Fields
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | camelCase identifier (becomes TS function name and C++ handler name) |
description |
string |
No | Documentation string |
method |
string |
Yes | "GET", "POST", "PUT", "DELETE", or "PATCH" |
path |
string |
Yes | URL path, with {param} placeholders for path parameters |
pathParams |
Record<string, Field> |
No | Path parameter definitions. Required if path contains {param} |
request |
Record<string, Field> |
No | Request body fields. Omit for no-body endpoints |
response |
Record<string, Field> |
No | Response fields (used for TypeScript types only) |
rawBody |
boolean |
No | When true, body is passed as-is (no JSON field mapping) |
Field Properties
| Field | Type | Required | Description |
|---|---|---|---|
type |
string |
Yes | "string", "number", "boolean", "number[]", "string[]", "object" |
description |
string |
No | Documentation string |
optional |
boolean |
No | Whether the field is optional (default: false) |
enum |
string[] |
No | Allowed values (generates TypeScript union type) |
pattern |
string |
No | Regex character class for path params (default: "[^/]+") |
Generated Files
1. C++ Route Registration (CustomRouteRegistration.cpp)
Wires each route to the HTTP server. The generator selects between simple and pattern-based registration depending on whether the route has path parameters.
// Auto-generated by NUXP Codegen - DO NOT EDIT
#include "CustomRouteHandlers.h"
#include "HttpServer.hpp"
#include <string>
#include <vector>
void RegisterCustomRoutes() {
using namespace MyPluginHandlers;
// POST /beds/create - Creates a bed from the current selection
HttpServer::Post("/beds/create", [](const std::string& body) {
return HandleCreateBed(body);
});
// DELETE /beds/{id} - Deletes a bed by its ID
HttpServer::DeleteWithPattern(
R"(/beds/([a-zA-Z0-9_-]+))",
[](const std::string& body, const std::vector<std::string>& params) {
if (params.empty()) {
return std::string("{\"error\":\"missing_id\"}");
}
return HandleDeleteBed(params[0]);
});
// GET /doc/info - Returns document information
HttpServer::Get("/doc/info", [](const std::string&) {
return HandleGetDocInfo();
});
}
Pattern route generation: When a route path contains {param} placeholders, the generator:
- Replaces each
{param}with a regex capture group using the param’spattern(or[^/]+by default) - Uses
HttpServer::MethodWithPatterninstead ofHttpServer::Method - Passes captured groups as a
std::vector<std::string>to the handler
2. C++ Handler Declarations (CustomRouteHandlers.h)
Declares the handler functions that the developer must implement in their own .cpp files.
#pragma once
// Auto-generated by NUXP Codegen - DO NOT EDIT
// Implement these functions in your own .cpp files.
#include <string>
namespace MyPluginHandlers {
/**
* POST /beds/create - Creates a bed from the current selection
* @param body JSON: { bedId: string, bedName?: string }
* @returns JSON: { success: bool, bedId: string, area_sqm: number }
*/
std::string HandleCreateBed(const std::string& body);
/**
* DELETE /beds/{id} - Deletes a bed by its ID
* @param id Bed identifier
* @returns JSON: { success: bool, deleted: number }
*/
std::string HandleDeleteBed(const std::string& id);
/**
* GET /doc/info - Returns document information
* @returns JSON: { name: string, path: string, artboardCount: number }
*/
std::string HandleGetDocInfo();
} // namespace MyPluginHandlers
Handler signature rules – the generator determines the C++ function signature from the route definition:
| Route Characteristics | Generated C++ Signature |
|---|---|
| GET, no path params | std::string HandleFoo() |
| POST/PUT with body (or rawBody) | std::string HandleFoo(const std::string& body) |
| Path params only, no body | std::string HandleFoo(const std::string& id) |
| Path params + body | std::string HandleFoo(const std::string& id, const std::string& body) |
3. TypeScript Client (customRoutes.ts)
Typed client functions with request/response interfaces.
/**
* Custom Route Client
* Auto-generated by NUXP Codegen - DO NOT EDIT
*/
import { getApiUrl } from '../config';
// Request/Response Types
/** POST /beds/create - Creates a bed from the current selection */
export interface CreateBedRequest {
/** Unique bed identifier */
bedId: string;
/** Display name */
bedName?: string;
}
export interface CreateBedResponse {
/** Whether creation succeeded */
success: boolean;
/** The created bed's UUID */
bedId: string;
/** Bed area in square meters */
area_sqm: number;
}
/** DELETE /beds/{id} */
export interface DeleteBedResponse {
success: boolean;
deleted: number;
}
/** GET /doc/info */
export interface GetDocInfoResponse {
name: string;
path: string;
artboardCount: number;
}
// Route Functions
/** POST /beds/create - Creates a bed from the current selection */
export async function createBed(params: CreateBedRequest): Promise<CreateBedResponse> {
return fetchRoute('POST', getApiUrl('/beds/create'), JSON.stringify(params));
}
/** DELETE /beds/{id} - Deletes a bed by its ID */
export async function deleteBed(id: string): Promise<DeleteBedResponse> {
return fetchRoute('DELETE', getApiUrl(`/beds/${encodeURIComponent(id)}`));
}
/** GET /doc/info - Returns document information */
export async function getDocInfo(): Promise<GetDocInfoResponse> {
return fetchRoute('GET', getApiUrl('/doc/info'));
}
TypeScript function signature rules:
| Route Characteristics | Generated TS Signature |
|---|---|
| No path params, no request | functionName(): Promise<Response> |
| No path params, has request | functionName(params: Request): Promise<Response> |
| Path params, no request | functionName(param1: string, ...): Promise<Response> |
| Path params + request | functionName(param1: string, ..., params: Request): Promise<Response> |
| rawBody (any) | functionName(...pathParams, body: string): Promise<Response> |
Usage: Adding a New Route
Step 1: Define the route in routes.json
{
"name": "getPlantInfo",
"description": "Returns metadata for a plant by UUID",
"method": "GET",
"path": "/plant/{uuid}/info",
"pathParams": {
"uuid": { "type": "string", "pattern": "[a-f0-9-]+", "description": "Plant UUID" }
},
"response": {
"uuid": { "type": "string" },
"scientificName": { "type": "string" },
"commonName": { "type": "string" },
"spreadDiameter": { "type": "number" }
}
}
Step 2: Run codegen
npm run generate
This produces updated CustomRouteRegistration.cpp, CustomRouteHandlers.h, and customRoutes.ts.
Step 3: Implement the C++ handler
Create (or update) your own .cpp file – codegen never overwrites implementation files:
#include "CustomRouteHandlers.h"
namespace MyPluginHandlers {
std::string HandleGetPlantInfo(const std::string& uuid) {
return QueueAndWait([uuid]() -> std::string {
// Your Illustrator SDK logic here
nlohmann::json result;
result["uuid"] = uuid;
result["scientificName"] = "Quercus virginiana";
result["commonName"] = "Live Oak";
result["spreadDiameter"] = 12.0;
return result.dump();
});
}
} // namespace MyPluginHandlers
Step 4: Use the generated TypeScript client
import { getPlantInfo } from '@/sdk/generated/customRoutes';
const plant = await getPlantInfo('abc-123');
console.log(plant.scientificName); // Fully typed
Config Inheritance (extends)
Downstream projects can extend a base config. This follows the same pattern as SSE events.
NUXP base: codegen/src/config/routes.json -> { "routes": [] }
Flora extends: flora-uxp/codegen/config/routes.json -> { "extends": "...", "routes": [...] }
Merge strategy:
- Load the base config recursively (base configs can themselves extend other configs)
- Extension routes override base routes with the same
name - Extension’s
namespacetakes precedence over the base’s - Combined result = base routes (minus those overridden) + extension routes
Example:
{
"extends": "../nuxp/codegen/src/config/routes.json",
"namespace": "FloraHandlers",
"routes": [
{
"name": "createBed",
"description": "Flora-specific bed creation",
"method": "POST",
"path": "/beds/create",
"request": { "bedId": { "type": "string" } },
"response": { "success": { "type": "boolean" } }
}
]
}
If the base defines createBed and the extension also defines createBed, the extension’s definition wins.
Validation Rules
The generator validates the config on load and throws on any violation:
- Each route must have
name,method, andpath methodmust be one ofGET,POST,PUT,DELETE,PATCH- Every
{param}placeholder inpathmust have a corresponding entry inpathParams rawBody: trueandrequestcannot coexist on the same route (you get one or the other)- No duplicate route names across all routes (including inherited ones after merge)
- No duplicate method + path combinations
Type Mappings
routes.json Type |
C++ Type | TypeScript Type |
|---|---|---|
"string" |
std::string |
string |
"number" |
double |
number |
"boolean" |
bool |
boolean |
"number[]" |
std::vector<double> |
number[] |
"string[]" |
std::vector<std::string> |
string[] |
"object" |
nlohmann::json |
Record<string, unknown> |
Enum fields generate TypeScript union types:
{ "type": "string", "enum": ["circle", "svg", "symbol"] }
Produces: 'circle' | 'svg' | 'symbol'
Optional fields generate optional TypeScript properties:
{ "type": "string", "optional": true }
Produces: fieldName?: string
Integration with the Codegen Pipeline
Custom route generation is integrated into codegen/src/index.ts and runs after SSE generation and before CMake include generation:
SDK Headers -> SuiteParser -> CppGenerator + TypeScriptGenerator
events.json -> SSEGenerator -> Events.hpp + events.ts
routes.json -> CustomRouteGenerator -> Registration.cpp + Handlers.h + customRoutes.ts
The integration follows the same pattern as SSE generation:
- Resolve the config path:
codegen/src/config/routes.json - Check if the file exists with
fs.pathExists() - If present, instantiate
CustomRouteGeneratorand call its three generation methods - Write output files to the C++ and TypeScript output directories
- Errors are caught and appended to the errors array (non-fatal to the rest of the pipeline)
- If the config file doesn’t exist, generation is silently skipped
No changes are needed to scripts/generate.sh – the existing file copy logic already picks up all .cpp, .h, .hpp, and .ts files from the output directories.
File Layout
codegen/src/
config/
events.json <- SSE events (existing)
routes.json <- Custom routes (new)
type-map.json <- SDK type mappings (existing)
generator/
SSEGenerator.ts <- SSE codegen (existing, pattern to follow)
CustomRouteGenerator.ts <- Custom route codegen (new)
CppGenerator.ts <- SDK wrapper codegen (existing)
TypeScriptGenerator.ts <- SDK client codegen (existing)
index.ts <- Pipeline entry point (modified)
Path Parameter Regex Generation
When a route has pathParams, the generator converts the path template to a C++ regex string:
| Path Template | pathParams | Generated Regex |
|---|---|---|
/plant/{uuid}/display-mode |
{ uuid: { pattern: "[a-f0-9-]+" } } |
/plant/([a-f0-9-]+)/display-mode |
/beds/{id} |
{ id: { pattern: "[a-zA-Z0-9_-]+" } } |
/beds/([a-zA-Z0-9_-]+) |
/items/{id} |
{ id: {} } |
/items/([^/]+) |
If no pattern is specified for a path parameter, it defaults to [^/]+ (any non-slash characters).
The HTTP method also changes from HttpServer::Method to HttpServer::MethodWithPattern for pattern routes.
rawBody Routes
When rawBody: true is set, the request body is not parsed into named fields. Instead, the entire body string is passed directly to the handler.
{
"name": "placeBatch",
"description": "Places multiple plants from a JSON array",
"method": "POST",
"path": "/plant/place-batch",
"rawBody": true,
"response": {
"success": { "type": "boolean" },
"placed": { "type": "number" }
}
}
- C++ side: Handler receives
const std::string& bodycontaining the raw JSON - TypeScript side: Function accepts
body: stringinstead of a typed params object
This is useful for endpoints that accept arrays, complex nested objects, or formats that don’t map cleanly to flat field definitions.