Skip to the content.

Custom Route Code Generation Specification

Captain NUXP

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:

  1. Replaces each {param} with a regex capture group using the param’s pattern (or [^/]+ by default)
  2. Uses HttpServer::MethodWithPattern instead of HttpServer::Method
  3. 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:

  1. Load the base config recursively (base configs can themselves extend other configs)
  2. Extension routes override base routes with the same name
  3. Extension’s namespace takes precedence over the base’s
  4. 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:

  1. Each route must have name, method, and path
  2. method must be one of GET, POST, PUT, DELETE, PATCH
  3. Every {param} placeholder in path must have a corresponding entry in pathParams
  4. rawBody: true and request cannot coexist on the same route (you get one or the other)
  5. No duplicate route names across all routes (including inherited ones after merge)
  6. 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:

  1. Resolve the config path: codegen/src/config/routes.json
  2. Check if the file exists with fs.pathExists()
  3. If present, instantiate CustomRouteGenerator and call its three generation methods
  4. Write output files to the C++ and TypeScript output directories
  5. Errors are caught and appended to the errors array (non-fatal to the rest of the pipeline)
  6. 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" }
  }
}

This is useful for endpoints that accept arrays, complex nested objects, or formats that don’t map cleanly to flat field definitions.