You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2023/09/11 18:03:43 UTC

[solr] branch branch_9x updated: SOLR-16825: Migrate api definitions to submodule, pt 3 (#1902)

This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new 44ff92b6f74 SOLR-16825: Migrate api definitions to submodule, pt 3 (#1902)
44ff92b6f74 is described below

commit 44ff92b6f740f8bc7c402d41eafc88d49446f335
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Mon Sep 11 13:58:40 2023 -0400

    SOLR-16825: Migrate api definitions to submodule, pt 3 (#1902)
    
    This commit covers the rename-collection, get-public-key, sync-shard, balance-
    replicas, force-leader, and replace-node APIs.
    
    Extracting annotated interfaces for these APIs includes them in the SolrRequest-
    generation we now do in SolrJ.
---
 .../client/api/endpoint/BalanceReplicasApi.java}   | 34 +++++++------
 .../solr/client/api/endpoint/ForceLeaderApi.java   | 34 +++++++++++++
 .../solr/client/api/endpoint/GetPublicKeyApi.java} | 29 ++++++-----
 .../client/api/endpoint/RenameCollectionApi.java   | 37 ++++++++++++++
 .../solr/client/api/endpoint/ReplaceNodeApi.java   | 45 +++++++++++++++++
 .../solr/client/api/endpoint/SyncShardApi.java     | 37 ++++++++++++++
 .../api/model/BalanceReplicasRequestBody.java      | 50 +++++++++++++++++++
 .../solr/client/api/model/PublicKeyResponse.java}  | 24 +++------
 .../api/model/RenameCollectionRequestBody.java}    | 23 ++++-----
 .../client/api/model/ReplaceNodeRequestBody.java   | 50 +++++++++++++++++++
 .../solr/handler/admin/CollectionsHandler.java     | 30 ++++++------
 ...alanceReplicasAPI.java => BalanceReplicas.java} | 54 +++-----------------
 .../api/{ForceLeaderAPI.java => ForceLeader.java}  | 25 +++-------
 ...ameCollectionAPI.java => RenameCollection.java} | 41 +++++-----------
 .../api/{ReplaceNodeAPI.java => ReplaceNode.java}  | 57 +++-------------------
 .../api/{SyncShardAPI.java => SyncShard.java}      | 24 +++------
 .../{PublicKeyAPI.java => GetPublicKey.java}       | 26 +++-------
 .../org/apache/solr/security/PublicKeyHandler.java |  4 +-
 .../org/apache/solr/cloud/BalanceReplicasTest.java |  9 ++--
 .../solr/handler/admin/api/ForceLeaderAPITest.java | 10 ++--
 .../handler/admin/api/MigrateReplicasAPITest.java  |  2 +-
 .../solr/handler/admin/api/ReplaceNodeAPITest.java | 13 +++--
 .../solr/handler/admin/api/SyncShardAPITest.java   |  6 +--
 ...PublicKeyAPITest.java => GetPublicKeyTest.java} |  6 +--
 .../solrj/src/resources/java-template/api.mustache |  1 +
 25 files changed, 389 insertions(+), 282 deletions(-)

diff --git a/solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/BalanceReplicasApi.java
similarity index 50%
copy from solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java
copy to solr/api/src/java/org/apache/solr/client/api/endpoint/BalanceReplicasApi.java
index f75b32b85d9..d05719efea8 100644
--- a/solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/BalanceReplicasApi.java
@@ -6,7 +6,7 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
- *      http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,21 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.solr.client.api.endpoint;
 
-package org.apache.solr.security;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import org.apache.solr.client.api.model.BalanceReplicasRequestBody;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
 
-import org.apache.solr.SolrTestCaseJ4;
-import org.junit.Test;
-
-/** Unit test for {@link PublicKeyAPI} */
-public class PublicKeyAPITest extends SolrTestCaseJ4 {
-
-  @Test
-  public void testRetrievesPublicKey() {
-    final SolrNodeKeyPair nodeKeyPair = new SolrNodeKeyPair(null);
-
-    final PublicKeyAPI.PublicKeyResponse response = new PublicKeyAPI(nodeKeyPair).getPublicKey();
-
-    assertEquals(nodeKeyPair.getKeyPair().getPublicKeyStr(), response.key);
-  }
+@Path("cluster/replicas/balance")
+public interface BalanceReplicasApi {
+  @POST
+  @Operation(
+      summary = "Balance Replicas across the given set of Nodes.",
+      tags = {"cluster"})
+  SolrJerseyResponse balanceReplicas(
+      @RequestBody(description = "Contains user provided parameters")
+          BalanceReplicasRequestBody requestBody)
+      throws Exception;
 }
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/ForceLeaderApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/ForceLeaderApi.java
new file mode 100644
index 00000000000..2a0336a2d3d
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ForceLeaderApi.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/** V2 API definition for triggering a leader election on a particular collection and shard. */
+@Path("/collections/{collectionName}/shards/{shardName}/force-leader")
+public interface ForceLeaderApi {
+  @POST
+  @Operation(
+      summary = "Force leader election to occur on the specified collection and shard",
+      tags = {"shards"})
+  SolrJerseyResponse forceShardLeader(
+      @PathParam("collectionName") String collectionName, @PathParam("shardName") String shardName);
+}
diff --git a/solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/GetPublicKeyApi.java
similarity index 58%
copy from solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java
copy to solr/api/src/java/org/apache/solr/client/api/endpoint/GetPublicKeyApi.java
index f75b32b85d9..562a2945d9e 100644
--- a/solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/GetPublicKeyApi.java
@@ -6,7 +6,7 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
- *      http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,21 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.solr.client.api.endpoint;
 
-package org.apache.solr.security;
+import io.swagger.v3.oas.annotations.Operation;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import org.apache.solr.client.api.model.PublicKeyResponse;
 
-import org.apache.solr.SolrTestCaseJ4;
-import org.junit.Test;
+/** V2 API definition to fetch the public key of the receiving node. */
+@Path("/node/key")
+public interface GetPublicKeyApi {
 
-/** Unit test for {@link PublicKeyAPI} */
-public class PublicKeyAPITest extends SolrTestCaseJ4 {
-
-  @Test
-  public void testRetrievesPublicKey() {
-    final SolrNodeKeyPair nodeKeyPair = new SolrNodeKeyPair(null);
-
-    final PublicKeyAPI.PublicKeyResponse response = new PublicKeyAPI(nodeKeyPair).getPublicKey();
-
-    assertEquals(nodeKeyPair.getKeyPair().getPublicKeyStr(), response.key);
-  }
+  @GET
+  @Operation(
+      summary = "Retrieve the public key of the receiving Solr node.",
+      tags = {"node"})
+  PublicKeyResponse getPublicKey();
 }
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/RenameCollectionApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/RenameCollectionApi.java
new file mode 100644
index 00000000000..6eee0d431f2
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/RenameCollectionApi.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import org.apache.solr.client.api.model.RenameCollectionRequestBody;
+import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
+
+/** V2 API definition for "renaming" an existing collection */
+@Path("/collections/{collectionName}/rename")
+public interface RenameCollectionApi {
+
+  @POST
+  @Operation(
+      summary = "Rename a SolrCloud collection",
+      tags = {"collections"})
+  SubResponseAccumulatingJerseyResponse renameCollection(
+      @PathParam("collectionName") String collectionName, RenameCollectionRequestBody requestBody)
+      throws Exception;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplaceNodeApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplaceNodeApi.java
new file mode 100644
index 00000000000..56762fa524d
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ReplaceNodeApi.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import org.apache.solr.client.api.model.ReplaceNodeRequestBody;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/**
+ * V2 API definition for recreating replicas in one node (the source) on another node(s) (the
+ * target).
+ */
+@Path("cluster/nodes/{sourceNodeName}/replace/")
+public interface ReplaceNodeApi {
+  @POST
+  @Operation(
+      summary = "'Replace' a specified node by moving all replicas elsewhere",
+      tags = {"node"})
+  SolrJerseyResponse replaceNode(
+      @Parameter(description = "The name of the node to be replaced.", required = true)
+          @PathParam("sourceNodeName")
+          String sourceNodeName,
+      @RequestBody(description = "Contains user provided parameters", required = true)
+          ReplaceNodeRequestBody requestBody)
+      throws Exception;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/endpoint/SyncShardApi.java b/solr/api/src/java/org/apache/solr/client/api/endpoint/SyncShardApi.java
new file mode 100644
index 00000000000..1e9805be65a
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/SyncShardApi.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.client.api.endpoint;
+
+import io.swagger.v3.oas.annotations.Operation;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import org.apache.solr.client.api.model.SolrJerseyResponse;
+
+/**
+ * V2 API definition for triggering a shard-sync operation within a particular collection and shard.
+ */
+@Path("/collections/{collectionName}/shards/{shardName}/sync")
+public interface SyncShardApi {
+  @POST
+  @Operation(
+      summary = "Trigger a 'sync' operation for the specified shard",
+      tags = {"shards"})
+  SolrJerseyResponse syncShard(
+      @PathParam("collectionName") String collectionName, @PathParam("shardName") String shardName)
+      throws Exception;
+}
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/BalanceReplicasRequestBody.java b/solr/api/src/java/org/apache/solr/client/api/model/BalanceReplicasRequestBody.java
new file mode 100644
index 00000000000..f270a48b269
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/BalanceReplicasRequestBody.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.Set;
+
+public class BalanceReplicasRequestBody {
+
+  public BalanceReplicasRequestBody() {}
+
+  public BalanceReplicasRequestBody(Set<String> nodes, Boolean waitForFinalState, String async) {
+    this.nodes = nodes;
+    this.waitForFinalState = waitForFinalState;
+    this.async = async;
+  }
+
+  @Schema(
+      description =
+          "The set of nodes across which replicas will be balanced. Defaults to all live data nodes.")
+  @JsonProperty(value = "nodes")
+  public Set<String> nodes;
+
+  @Schema(
+      description =
+          "If true, the request will complete only when all affected replicas become active. "
+              + "If false, the API will return the status of the single action, which may be "
+              + "before the new replica is online and active.")
+  @JsonProperty("waitForFinalState")
+  public Boolean waitForFinalState = false;
+
+  @Schema(description = "Request ID to track this action which will be processed asynchronously.")
+  @JsonProperty("async")
+  public String async;
+}
diff --git a/solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java b/solr/api/src/java/org/apache/solr/client/api/model/PublicKeyResponse.java
similarity index 58%
copy from solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java
copy to solr/api/src/java/org/apache/solr/client/api/model/PublicKeyResponse.java
index f75b32b85d9..d561b16fcbb 100644
--- a/solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java
+++ b/solr/api/src/java/org/apache/solr/client/api/model/PublicKeyResponse.java
@@ -6,7 +6,7 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
- *      http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,21 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.solr.client.api.model;
 
-package org.apache.solr.security;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
 
-import org.apache.solr.SolrTestCaseJ4;
-import org.junit.Test;
-
-/** Unit test for {@link PublicKeyAPI} */
-public class PublicKeyAPITest extends SolrTestCaseJ4 {
-
-  @Test
-  public void testRetrievesPublicKey() {
-    final SolrNodeKeyPair nodeKeyPair = new SolrNodeKeyPair(null);
-
-    final PublicKeyAPI.PublicKeyResponse response = new PublicKeyAPI(nodeKeyPair).getPublicKey();
-
-    assertEquals(nodeKeyPair.getKeyPair().getPublicKeyStr(), response.key);
-  }
+public class PublicKeyResponse extends SolrJerseyResponse {
+  @JsonProperty("key")
+  @Schema(description = "The public key of the receiving Solr node.")
+  public String key;
 }
diff --git a/solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java b/solr/api/src/java/org/apache/solr/client/api/model/RenameCollectionRequestBody.java
similarity index 58%
copy from solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java
copy to solr/api/src/java/org/apache/solr/client/api/model/RenameCollectionRequestBody.java
index f75b32b85d9..145618d029e 100644
--- a/solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java
+++ b/solr/api/src/java/org/apache/solr/client/api/model/RenameCollectionRequestBody.java
@@ -6,7 +6,7 @@
  * (the "License"); you may not use this file except in compliance with
  * the License.  You may obtain a copy of the License at
  *
- *      http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,21 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.solr.client.api.model;
 
-package org.apache.solr.security;
+import static org.apache.solr.client.api.model.Constants.ASYNC;
 
-import org.apache.solr.SolrTestCaseJ4;
-import org.junit.Test;
+import com.fasterxml.jackson.annotation.JsonProperty;
 
-/** Unit test for {@link PublicKeyAPI} */
-public class PublicKeyAPITest extends SolrTestCaseJ4 {
+public class RenameCollectionRequestBody {
+  @JsonProperty(required = true)
+  public String to;
 
-  @Test
-  public void testRetrievesPublicKey() {
-    final SolrNodeKeyPair nodeKeyPair = new SolrNodeKeyPair(null);
+  @JsonProperty(ASYNC)
+  public String async;
 
-    final PublicKeyAPI.PublicKeyResponse response = new PublicKeyAPI(nodeKeyPair).getPublicKey();
-
-    assertEquals(nodeKeyPair.getKeyPair().getPublicKeyStr(), response.key);
-  }
+  @JsonProperty public Boolean followAliases;
 }
diff --git a/solr/api/src/java/org/apache/solr/client/api/model/ReplaceNodeRequestBody.java b/solr/api/src/java/org/apache/solr/client/api/model/ReplaceNodeRequestBody.java
new file mode 100644
index 00000000000..303fd64e8db
--- /dev/null
+++ b/solr/api/src/java/org/apache/solr/client/api/model/ReplaceNodeRequestBody.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.client.api.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+public class ReplaceNodeRequestBody {
+
+  public ReplaceNodeRequestBody() {}
+
+  public ReplaceNodeRequestBody(String targetNodeName, Boolean waitForFinalState, String async) {
+    this.targetNodeName = targetNodeName;
+    this.waitForFinalState = waitForFinalState;
+    this.async = async;
+  }
+
+  @Schema(
+      description =
+          "The target node where replicas will be copied. If this parameter is not provided, Solr "
+              + "will identify nodes automatically based on policies or number of cores in each node.")
+  @JsonProperty("targetNodeName")
+  public String targetNodeName;
+
+  @Schema(
+      description =
+          "If true, the request will complete only when all affected replicas become active. "
+              + "If false, the API will return the status of the single action, which may be "
+              + "before the new replica is online and active.")
+  @JsonProperty("waitForFinalState")
+  public Boolean waitForFinalState = false;
+
+  @Schema(description = "Request ID to track this action which will be processed asynchronously.")
+  @JsonProperty("async")
+  public String async;
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index b1949c993a1..97de7a36bb3 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -123,6 +123,7 @@ import org.apache.solr.api.AnnotatedApi;
 import org.apache.solr.api.Api;
 import org.apache.solr.api.JerseyResource;
 import org.apache.solr.client.api.model.AddReplicaPropertyRequestBody;
+import org.apache.solr.client.api.model.ReplaceNodeRequestBody;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.api.model.UpdateAliasPropertiesRequestBody;
 import org.apache.solr.client.solrj.SolrResponse;
@@ -166,7 +167,7 @@ import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.handler.admin.api.AddReplicaProperty;
 import org.apache.solr.handler.admin.api.AdminAPIBase;
 import org.apache.solr.handler.admin.api.AliasProperty;
-import org.apache.solr.handler.admin.api.BalanceReplicasAPI;
+import org.apache.solr.handler.admin.api.BalanceReplicas;
 import org.apache.solr.handler.admin.api.BalanceShardUniqueAPI;
 import org.apache.solr.handler.admin.api.CollectionPropertyAPI;
 import org.apache.solr.handler.admin.api.CollectionStatusAPI;
@@ -184,7 +185,7 @@ import org.apache.solr.handler.admin.api.DeleteNode;
 import org.apache.solr.handler.admin.api.DeleteReplica;
 import org.apache.solr.handler.admin.api.DeleteReplicaProperty;
 import org.apache.solr.handler.admin.api.DeleteShardAPI;
-import org.apache.solr.handler.admin.api.ForceLeaderAPI;
+import org.apache.solr.handler.admin.api.ForceLeader;
 import org.apache.solr.handler.admin.api.InstallShardDataAPI;
 import org.apache.solr.handler.admin.api.ListAliases;
 import org.apache.solr.handler.admin.api.ListCollectionBackups;
@@ -196,11 +197,11 @@ import org.apache.solr.handler.admin.api.ModifyCollectionAPI;
 import org.apache.solr.handler.admin.api.MoveReplicaAPI;
 import org.apache.solr.handler.admin.api.RebalanceLeadersAPI;
 import org.apache.solr.handler.admin.api.ReloadCollectionAPI;
-import org.apache.solr.handler.admin.api.RenameCollectionAPI;
-import org.apache.solr.handler.admin.api.ReplaceNodeAPI;
+import org.apache.solr.handler.admin.api.RenameCollection;
+import org.apache.solr.handler.admin.api.ReplaceNode;
 import org.apache.solr.handler.admin.api.RestoreCollectionAPI;
 import org.apache.solr.handler.admin.api.SplitShardAPI;
-import org.apache.solr.handler.admin.api.SyncShardAPI;
+import org.apache.solr.handler.admin.api.SyncShard;
 import org.apache.solr.handler.api.V2ApiUtils;
 import org.apache.solr.logging.MDCLoggingContext;
 import org.apache.solr.request.SolrQueryRequest;
@@ -607,7 +608,7 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     SYNCSHARD_OP(
         SYNCSHARD,
         (req, rsp, h) -> {
-          SyncShardAPI.invokeFromV1Params(h.coreContainer, req, rsp);
+          SyncShard.invokeFromV1Params(h.coreContainer, req, rsp);
           return null;
         }),
 
@@ -722,7 +723,7 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     FORCELEADER_OP(
         FORCELEADER,
         (req, rsp, h) -> {
-          ForceLeaderAPI.invokeFromV1Params(h.coreContainer, req, rsp);
+          ForceLeader.invokeFromV1Params(h.coreContainer, req, rsp);
           return null;
         }),
     CREATESHARD_OP(
@@ -1175,12 +1176,11 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
         (req, rsp, h) -> {
           final SolrParams params = req.getParams();
           final RequiredSolrParams requiredParams = req.getParams().required();
-          final ReplaceNodeAPI.ReplaceNodeRequestBody requestBody =
-              new ReplaceNodeAPI.ReplaceNodeRequestBody();
+          final var requestBody = new ReplaceNodeRequestBody();
           requestBody.targetNodeName = params.get(TARGET_NODE);
           requestBody.waitForFinalState = params.getBool(WAIT_FOR_FINAL_STATE);
           requestBody.async = params.get(ASYNC);
-          final ReplaceNodeAPI replaceNodeAPI = new ReplaceNodeAPI(h.coreContainer, req, rsp);
+          final ReplaceNode replaceNodeAPI = new ReplaceNode(h.coreContainer, req, rsp);
           final SolrJerseyResponse replaceNodeResponse =
               replaceNodeAPI.replaceNode(requiredParams.get(SOURCE_NODE), requestBody);
           V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, replaceNodeResponse);
@@ -1375,17 +1375,17 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
         DeleteReplica.class,
         DeleteReplicaProperty.class,
         DeleteShardAPI.class,
-        ForceLeaderAPI.class,
+        ForceLeader.class,
         InstallShardDataAPI.class,
         ListCollections.class,
         ListCollectionBackups.class,
         ReloadCollectionAPI.class,
-        RenameCollectionAPI.class,
-        ReplaceNodeAPI.class,
+        RenameCollection.class,
+        ReplaceNode.class,
         MigrateReplicasAPI.class,
-        BalanceReplicasAPI.class,
+        BalanceReplicas.class,
         RestoreCollectionAPI.class,
-        SyncShardAPI.class,
+        SyncShard.class,
         CollectionPropertyAPI.class,
         DeleteNode.class,
         ListAliases.class,
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceReplicasAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceReplicas.java
similarity index 63%
rename from solr/core/src/java/org/apache/solr/handler/admin/api/BalanceReplicasAPI.java
rename to solr/core/src/java/org/apache/solr/handler/admin/api/BalanceReplicas.java
index 53e23886585..6e163857296 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceReplicasAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/BalanceReplicas.java
@@ -16,7 +16,6 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
 import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.params.CollectionParams.NODES;
 import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
@@ -24,47 +23,35 @@ import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STA
 import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.parameters.RequestBody;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Set;
 import javax.inject.Inject;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
+import org.apache.solr.client.api.endpoint.BalanceReplicasApi;
+import org.apache.solr.client.api.model.BalanceReplicasRequestBody;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams.CollectionAction;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.handler.admin.CollectionsHandler;
-import org.apache.solr.jersey.JacksonReflectMapWriter;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
 /** V2 API for balancing the replicas that already exist across a set of nodes. */
-@Path("cluster/replicas/balance")
-public class BalanceReplicasAPI extends AdminAPIBase {
+public class BalanceReplicas extends AdminAPIBase implements BalanceReplicasApi {
 
   @Inject
-  public BalanceReplicasAPI(
+  public BalanceReplicas(
       CoreContainer coreContainer,
       SolrQueryRequest solrQueryRequest,
       SolrQueryResponse solrQueryResponse) {
     super(coreContainer, solrQueryRequest, solrQueryResponse);
   }
 
-  @POST
-  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @Override
   @PermissionName(COLL_EDIT_PERM)
-  @Operation(summary = "Balance Replicas across the given set of Nodes.")
-  public SolrJerseyResponse balanceReplicas(
-      @RequestBody(description = "Contains user provided parameters")
-          BalanceReplicasRequestBody requestBody)
+  public SolrJerseyResponse balanceReplicas(BalanceReplicasRequestBody requestBody)
       throws Exception {
     final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
     final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
@@ -96,33 +83,4 @@ public class BalanceReplicasAPI extends AdminAPIBase {
 
     return new ZkNodeProps(remoteMessage);
   }
-
-  public static class BalanceReplicasRequestBody implements JacksonReflectMapWriter {
-
-    public BalanceReplicasRequestBody() {}
-
-    public BalanceReplicasRequestBody(Set<String> nodes, Boolean waitForFinalState, String async) {
-      this.nodes = nodes;
-      this.waitForFinalState = waitForFinalState;
-      this.async = async;
-    }
-
-    @Schema(
-        description =
-            "The set of nodes across which replicas will be balanced. Defaults to all live data nodes.")
-    @JsonProperty(value = "nodes")
-    public Set<String> nodes;
-
-    @Schema(
-        description =
-            "If true, the request will complete only when all affected replicas become active. "
-                + "If false, the API will return the status of the single action, which may be "
-                + "before the new replica is online and active.")
-    @JsonProperty("waitForFinalState")
-    public Boolean waitForFinalState = false;
-
-    @Schema(description = "Request ID to track this action which will be processed asynchronously.")
-    @JsonProperty("async")
-    public String async;
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ForceLeaderAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ForceLeader.java
similarity index 88%
rename from solr/core/src/java/org/apache/solr/handler/admin/api/ForceLeaderAPI.java
rename to solr/core/src/java/org/apache/solr/handler/admin/api/ForceLeader.java
index b0881a34bac..c3080e3ef16 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ForceLeaderAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ForceLeader.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
@@ -27,11 +26,7 @@ import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.inject.Inject;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
+import org.apache.solr.client.api.endpoint.ForceLeaderApi;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.cloud.ZkController;
 import org.apache.solr.cloud.ZkShardTerms;
@@ -49,29 +44,25 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * V2 API for triggering a leader election on a particular collection and shard.
+ * V2 API implementation for triggering a leader election on a particular collection and shard.
  *
  * <p>This API (POST /v2/collections/collectionName/shards/shardName/force-leader) is analogous to
  * the v1 /admin/collections?action=FORCELEADER command.
  */
-@Path("/collections/{collectionName}/shards/{shardName}/force-leader")
-public class ForceLeaderAPI extends AdminAPIBase {
+public class ForceLeader extends AdminAPIBase implements ForceLeaderApi {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   @Inject
-  public ForceLeaderAPI(
+  public ForceLeader(
       CoreContainer coreContainer,
       SolrQueryRequest solrQueryRequest,
       SolrQueryResponse solrQueryResponse) {
     super(coreContainer, solrQueryRequest, solrQueryResponse);
   }
 
-  @POST
-  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @Override
   @PermissionName(COLL_EDIT_PERM)
-  public SolrJerseyResponse forceLeader(
-      @PathParam("collectionName") String collectionName,
-      @PathParam("shardName") String shardName) {
+  public SolrJerseyResponse forceShardLeader(String collectionName, String shardName) {
     final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
     ensureRequiredParameterProvided(COLLECTION_PROP, collectionName);
     ensureRequiredParameterProvided(SHARD_ID_PROP, shardName);
@@ -84,12 +75,12 @@ public class ForceLeaderAPI extends AdminAPIBase {
 
   public static void invokeFromV1Params(
       CoreContainer coreContainer, SolrQueryRequest request, SolrQueryResponse response) {
-    final var api = new ForceLeaderAPI(coreContainer, request, response);
+    final var api = new ForceLeader(coreContainer, request, response);
     final var params = request.getParams();
     params.required().check(COLLECTION_PROP, SHARD_ID_PROP);
 
     V2ApiUtils.squashIntoSolrResponseWithoutHeader(
-        response, api.forceLeader(params.get(COLLECTION_PROP), params.get(SHARD_ID_PROP)));
+        response, api.forceShardLeader(params.get(COLLECTION_PROP), params.get(SHARD_ID_PROP)));
   }
 
   private void doForceLeaderElection(String extCollectionName, String shardName) {
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/RenameCollectionAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/RenameCollection.java
similarity index 76%
rename from solr/core/src/java/org/apache/solr/handler/admin/api/RenameCollectionAPI.java
rename to solr/core/src/java/org/apache/solr/handler/admin/api/RenameCollection.java
index 2fd2d7fb405..8cec8535a7f 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/RenameCollectionAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/RenameCollection.java
@@ -16,7 +16,6 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
 import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
@@ -25,48 +24,40 @@ import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
 import static org.apache.solr.common.params.CommonParams.NAME;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
 import java.util.HashMap;
 import java.util.Map;
 import javax.inject.Inject;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
+import org.apache.solr.client.api.endpoint.RenameCollectionApi;
+import org.apache.solr.client.api.model.RenameCollectionRequestBody;
 import org.apache.solr.client.api.model.SubResponseAccumulatingJerseyResponse;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.handler.api.V2ApiUtils;
-import org.apache.solr.jersey.JacksonReflectMapWriter;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
 /**
- * V2 API for "renaming" an existing collection
+ * V2 API implementation to "rename" an existing collection
  *
  * <p>This API is analogous to the v1 /admin/collections?action=RENAME command.
  */
-@Path("/collections/{collectionName}/rename")
-public class RenameCollectionAPI extends AdminAPIBase {
+public class RenameCollection extends AdminAPIBase implements RenameCollectionApi {
 
   @Inject
-  public RenameCollectionAPI(
+  public RenameCollection(
       CoreContainer coreContainer,
       SolrQueryRequest solrQueryRequest,
       SolrQueryResponse solrQueryResponse) {
     super(coreContainer, solrQueryRequest, solrQueryResponse);
   }
 
-  @POST
-  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @Override
   @PermissionName(COLL_EDIT_PERM)
   public SubResponseAccumulatingJerseyResponse renameCollection(
-      @PathParam("collectionName") String collectionName, RenameCollectionRequestBody requestBody)
-      throws Exception {
+      String collectionName, RenameCollectionRequestBody requestBody) throws Exception {
     final var response = instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
     if (requestBody == null) {
       throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing required request body");
@@ -81,7 +72,7 @@ public class RenameCollectionAPI extends AdminAPIBase {
         response,
         CollectionParams.CollectionAction.RENAME,
         remoteMessage,
-        requestBody != null ? requestBody.asyncId : null);
+        requestBody != null ? requestBody.async : null);
     return response;
   }
 
@@ -92,7 +83,7 @@ public class RenameCollectionAPI extends AdminAPIBase {
     remoteMessage.put(NAME, collectionName);
     remoteMessage.put(TARGET, requestBody.to);
     insertIfNotNull(remoteMessage, FOLLOW_ALIASES, requestBody.followAliases);
-    insertIfNotNull(remoteMessage, ASYNC, requestBody.asyncId);
+    insertIfNotNull(remoteMessage, ASYNC, requestBody.async);
 
     return new ZkNodeProps(remoteMessage);
   }
@@ -100,26 +91,16 @@ public class RenameCollectionAPI extends AdminAPIBase {
   public static void invokeFromV1Params(
       CoreContainer coreContainer, SolrQueryRequest request, SolrQueryResponse response)
       throws Exception {
-    final var api = new RenameCollectionAPI(coreContainer, request, response);
+    final var api = new RenameCollection(coreContainer, request, response);
     final var params = request.getParams();
     params.required().check(COLLECTION_PROP, TARGET);
     final var requestBody = new RenameCollectionRequestBody();
     requestBody.to = params.get(TARGET);
     // Optional parameters
-    requestBody.asyncId = params.get(ASYNC);
+    requestBody.async = params.get(ASYNC);
     requestBody.followAliases = params.getBool(FOLLOW_ALIASES);
 
     V2ApiUtils.squashIntoSolrResponseWithoutHeader(
         response, api.renameCollection(params.get(COLLECTION_PROP), requestBody));
   }
-
-  public static class RenameCollectionRequestBody implements JacksonReflectMapWriter {
-    @JsonProperty(required = true)
-    public String to;
-
-    @JsonProperty(ASYNC)
-    public String asyncId;
-
-    @JsonProperty public Boolean followAliases;
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNodeAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNode.java
similarity index 64%
rename from solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNodeAPI.java
rename to solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNode.java
index e2e63760ab7..ab597b54038 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNodeAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ReplaceNode.java
@@ -16,7 +16,6 @@
  */
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
 import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
 import static org.apache.solr.common.params.CollectionParams.SOURCE_NODE;
 import static org.apache.solr.common.params.CollectionParams.TARGET_NODE;
@@ -25,17 +24,11 @@ import static org.apache.solr.common.params.CommonAdminParams.WAIT_FOR_FINAL_STA
 import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.parameters.RequestBody;
 import java.util.HashMap;
 import java.util.Map;
 import javax.inject.Inject;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
+import org.apache.solr.client.api.endpoint.ReplaceNodeApi;
+import org.apache.solr.client.api.model.ReplaceNodeRequestBody;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.common.cloud.ZkNodeProps;
@@ -43,7 +36,6 @@ import org.apache.solr.common.params.CollectionParams;
 import org.apache.solr.common.params.CollectionParams.CollectionAction;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.handler.admin.CollectionsHandler;
-import org.apache.solr.jersey.JacksonReflectMapWriter;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -53,26 +45,19 @@ import org.apache.solr.response.SolrQueryResponse;
  *
  * <p>This API is analogous to the v1 /admin/collections?action=REPLACENODE command.
  */
-@Path("cluster/nodes/{sourceNodeName}/replace/")
-public class ReplaceNodeAPI extends AdminAPIBase {
+public class ReplaceNode extends AdminAPIBase implements ReplaceNodeApi {
 
   @Inject
-  public ReplaceNodeAPI(
+  public ReplaceNode(
       CoreContainer coreContainer,
       SolrQueryRequest solrQueryRequest,
       SolrQueryResponse solrQueryResponse) {
     super(coreContainer, solrQueryRequest, solrQueryResponse);
   }
 
-  @POST
-  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @Override
   @PermissionName(COLL_EDIT_PERM)
-  public SolrJerseyResponse replaceNode(
-      @Parameter(description = "The name of the node to be replaced.", required = true)
-          @PathParam("sourceNodeName")
-          String sourceNodeName,
-      @RequestBody(description = "Contains user provided parameters", required = true)
-          ReplaceNodeRequestBody requestBody)
+  public SolrJerseyResponse replaceNode(String sourceNodeName, ReplaceNodeRequestBody requestBody)
       throws Exception {
     final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
     final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
@@ -111,34 +96,4 @@ public class ReplaceNodeAPI extends AdminAPIBase {
       dest.put(key, value);
     }
   }
-
-  public static class ReplaceNodeRequestBody implements JacksonReflectMapWriter {
-
-    public ReplaceNodeRequestBody() {}
-
-    public ReplaceNodeRequestBody(String targetNodeName, Boolean waitForFinalState, String async) {
-      this.targetNodeName = targetNodeName;
-      this.waitForFinalState = waitForFinalState;
-      this.async = async;
-    }
-
-    @Schema(
-        description =
-            "The target node where replicas will be copied. If this parameter is not provided, Solr "
-                + "will identify nodes automatically based on policies or number of cores in each node.")
-    @JsonProperty("targetNodeName")
-    public String targetNodeName;
-
-    @Schema(
-        description =
-            "If true, the request will complete only when all affected replicas become active. "
-                + "If false, the API will return the status of the single action, which may be "
-                + "before the new replica is online and active.")
-    @JsonProperty("waitForFinalState")
-    public Boolean waitForFinalState = false;
-
-    @Schema(description = "Request ID to track this action which will be processed asynchronously.")
-    @JsonProperty("async")
-    public String async;
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/SyncShardAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/SyncShard.java
similarity index 83%
rename from solr/core/src/java/org/apache/solr/handler/admin/api/SyncShardAPI.java
rename to solr/core/src/java/org/apache/solr/handler/admin/api/SyncShard.java
index 1b7b44b0dea..b506f585673 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/SyncShardAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/SyncShard.java
@@ -17,7 +17,6 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.SHARD_ID_PROP;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
@@ -25,11 +24,7 @@ import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PER
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 import javax.inject.Inject;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
+import org.apache.solr.client.api.endpoint.SyncShardApi;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -46,28 +41,25 @@ import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
 /**
- * V2 API for triggering a shard-sync operation within a particular collection and shard.
+ * V2 API implementation for triggering a shard-sync operation within a particular collection and
+ * shard.
  *
  * <p>This API (POST /v2/collections/cName/shards/sName/sync {...}) is analogous to the v1
  * /admin/collections?action=SYNCSHARD command.
  */
-@Path("/collections/{collectionName}/shards/{shardName}/sync")
-public class SyncShardAPI extends AdminAPIBase {
+public class SyncShard extends AdminAPIBase implements SyncShardApi {
 
   @Inject
-  public SyncShardAPI(
+  public SyncShard(
       CoreContainer coreContainer,
       SolrQueryRequest solrQueryRequest,
       SolrQueryResponse solrQueryResponse) {
     super(coreContainer, solrQueryRequest, solrQueryResponse);
   }
 
-  @POST
-  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @Override
   @PermissionName(COLL_EDIT_PERM)
-  public SolrJerseyResponse syncShard(
-      @PathParam("collectionName") String collectionName, @PathParam("shardName") String shardName)
-      throws Exception {
+  public SolrJerseyResponse syncShard(String collectionName, String shardName) throws Exception {
     final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
     ensureRequiredParameterProvided(COLLECTION_PROP, collectionName);
     ensureRequiredParameterProvided(SHARD_ID_PROP, shardName);
@@ -105,7 +97,7 @@ public class SyncShardAPI extends AdminAPIBase {
   public static void invokeFromV1Params(
       CoreContainer coreContainer, SolrQueryRequest request, SolrQueryResponse response)
       throws Exception {
-    final var api = new SyncShardAPI(coreContainer, request, response);
+    final var api = new SyncShard(coreContainer, request, response);
     final var params = request.getParams();
     params.required().check(COLLECTION_PROP, SHARD_ID_PROP);
 
diff --git a/solr/core/src/java/org/apache/solr/security/PublicKeyAPI.java b/solr/core/src/java/org/apache/solr/security/GetPublicKey.java
similarity index 63%
rename from solr/core/src/java/org/apache/solr/security/PublicKeyAPI.java
rename to solr/core/src/java/org/apache/solr/security/GetPublicKey.java
index 46483462e5f..1f7e991667d 100644
--- a/solr/core/src/java/org/apache/solr/security/PublicKeyAPI.java
+++ b/solr/core/src/java/org/apache/solr/security/GetPublicKey.java
@@ -17,45 +17,31 @@
 
 package org.apache.solr.security;
 
-import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import io.swagger.v3.oas.annotations.media.Schema;
 import javax.inject.Inject;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
 import org.apache.solr.api.JerseyResource;
-import org.apache.solr.client.api.model.SolrJerseyResponse;
+import org.apache.solr.client.api.endpoint.GetPublicKeyApi;
+import org.apache.solr.client.api.model.PublicKeyResponse;
 import org.apache.solr.jersey.PermissionName;
 
 /**
- * V2 API for fetching the public key of the receiving node.
+ * V2 API implementation to fetch the public key of the receiving node.
  *
  * <p>This API is analogous to the v1 /admin/info/key endpoint.
  */
-@Path("/node/key")
-public class PublicKeyAPI extends JerseyResource {
+public class GetPublicKey extends JerseyResource implements GetPublicKeyApi {
 
   private final SolrNodeKeyPair nodeKeyPair;
 
   @Inject
-  public PublicKeyAPI(SolrNodeKeyPair nodeKeyPair) {
+  public GetPublicKey(SolrNodeKeyPair nodeKeyPair) {
     this.nodeKeyPair = nodeKeyPair;
   }
 
-  @GET
-  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @Override
   @PermissionName(PermissionNameProvider.Name.ALL)
   public PublicKeyResponse getPublicKey() {
     final PublicKeyResponse response = instantiateJerseyResponse(PublicKeyResponse.class);
     response.key = nodeKeyPair.getKeyPair().getPublicKeyStr();
     return response;
   }
-
-  public static class PublicKeyResponse extends SolrJerseyResponse {
-    @JsonProperty("key")
-    @Schema(description = "The public key of the receiving Solr node.")
-    public String key;
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/security/PublicKeyHandler.java b/solr/core/src/java/org/apache/solr/security/PublicKeyHandler.java
index 9dbde9d0966..5f2941fec23 100644
--- a/solr/core/src/java/org/apache/solr/security/PublicKeyHandler.java
+++ b/solr/core/src/java/org/apache/solr/security/PublicKeyHandler.java
@@ -50,7 +50,7 @@ public class PublicKeyHandler extends RequestHandlerBase {
   @Override
   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
     V2ApiUtils.squashIntoSolrResponseWithoutHeader(
-        rsp, new PublicKeyAPI(nodeKeyPair).getPublicKey());
+        rsp, new GetPublicKey(nodeKeyPair).getPublicKey());
   }
 
   @Override
@@ -80,6 +80,6 @@ public class PublicKeyHandler extends RequestHandlerBase {
 
   @Override
   public Collection<Class<? extends JerseyResource>> getJerseyResources() {
-    return List.of(PublicKeyAPI.class);
+    return List.of(GetPublicKey.class);
   }
 }
diff --git a/solr/core/src/test/org/apache/solr/cloud/BalanceReplicasTest.java b/solr/core/src/test/org/apache/solr/cloud/BalanceReplicasTest.java
index 82a75027e68..a14b372df2e 100644
--- a/solr/core/src/test/org/apache/solr/cloud/BalanceReplicasTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/BalanceReplicasTest.java
@@ -33,14 +33,15 @@ import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.entity.ContentType;
 import org.apache.http.util.EntityUtils;
+import org.apache.solr.client.api.model.BalanceReplicasRequestBody;
 import org.apache.solr.client.solrj.impl.CloudLegacySolrClient;
 import org.apache.solr.client.solrj.impl.CloudSolrClient;
 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.common.MapWriterMap;
 import org.apache.solr.common.cloud.DocCollection;
 import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.common.util.Utils;
-import org.apache.solr.handler.admin.api.BalanceReplicasAPI;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -101,7 +102,7 @@ public class BalanceReplicasTest extends SolrCloudTestCase {
     postDataAndGetResponse(
         cluster.getSolrClient(),
         "/api/cluster/replicas/balance",
-        BalanceReplicasAPI.BalanceReplicasRequestBody.EMPTY);
+        new MapWriterMap(Collections.emptyMap()));
 
     collection = cloudClient.getClusterState().getCollectionOrNull(coll, false);
     log.debug("### After balancing: {}", collection);
@@ -149,8 +150,8 @@ public class BalanceReplicasTest extends SolrCloudTestCase {
     postDataAndGetResponse(
         cluster.getSolrClient(),
         "/api/cluster/replicas/balance",
-        new BalanceReplicasAPI.BalanceReplicasRequestBody(
-            new HashSet<>(l.subList(1, 4)), true, null));
+        Utils.getReflectWriter(
+            new BalanceReplicasRequestBody(new HashSet<>(l.subList(1, 4)), true, null)));
 
     collection = cloudClient.getClusterState().getCollectionOrNull(coll, false);
     log.debug("### After balancing: {}", collection);
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/ForceLeaderAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/ForceLeaderAPITest.java
index 09f97a4757f..be1fce44fb8 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/ForceLeaderAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/ForceLeaderAPITest.java
@@ -21,7 +21,7 @@ import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.SolrException;
 import org.junit.Test;
 
-/** Unit tests for {@link ForceLeaderAPI} */
+/** Unit tests for {@link ForceLeader} */
 public class ForceLeaderAPITest extends SolrTestCaseJ4 {
   @Test
   public void testReportsErrorIfCollectionNameMissing() {
@@ -29,8 +29,8 @@ public class ForceLeaderAPITest extends SolrTestCaseJ4 {
         expectThrows(
             SolrException.class,
             () -> {
-              final var api = new ForceLeaderAPI(null, null, null);
-              api.forceLeader(null, "someShard");
+              final var api = new ForceLeader(null, null, null);
+              api.forceShardLeader(null, "someShard");
             });
 
     assertEquals(400, thrown.code());
@@ -43,8 +43,8 @@ public class ForceLeaderAPITest extends SolrTestCaseJ4 {
         expectThrows(
             SolrException.class,
             () -> {
-              final var api = new ForceLeaderAPI(null, null, null);
-              api.forceLeader("someCollection", null);
+              final var api = new ForceLeader(null, null, null);
+              api.forceShardLeader("someCollection", null);
             });
 
     assertEquals(400, thrown.code());
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/MigrateReplicasAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/MigrateReplicasAPITest.java
index e87a4c67532..f9bf865d4e0 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/MigrateReplicasAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/MigrateReplicasAPITest.java
@@ -40,7 +40,7 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 
-/** Unit tests for {@link ReplaceNodeAPI} */
+/** Unit tests for {@link ReplaceNode} */
 public class MigrateReplicasAPITest extends SolrTestCaseJ4 {
 
   private CoreContainer mockCoreContainer;
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/ReplaceNodeAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/ReplaceNodeAPITest.java
index 3a460245775..e49eb56de0d 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/ReplaceNodeAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/ReplaceNodeAPITest.java
@@ -25,6 +25,7 @@ import static org.mockito.Mockito.when;
 import java.util.Map;
 import java.util.Optional;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.client.api.model.ReplaceNodeRequestBody;
 import org.apache.solr.cloud.OverseerSolrResponse;
 import org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetCommandRunner;
 import org.apache.solr.common.cloud.ZkNodeProps;
@@ -37,13 +38,13 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 
-/** Unit tests for {@link ReplaceNodeAPI} */
+/** Unit tests for {@link ReplaceNode} */
 public class ReplaceNodeAPITest extends SolrTestCaseJ4 {
 
   private CoreContainer mockCoreContainer;
   private SolrQueryRequest mockQueryRequest;
   private SolrQueryResponse queryResponse;
-  private ReplaceNodeAPI replaceNodeApi;
+  private ReplaceNode replaceNodeApi;
   private DistributedCollectionConfigSetCommandRunner mockCommandRunner;
   private ArgumentCaptor<ZkNodeProps> messageCapturer;
 
@@ -65,7 +66,7 @@ public class ReplaceNodeAPITest extends SolrTestCaseJ4 {
         .thenReturn(new OverseerSolrResponse(new NamedList<>()));
     mockQueryRequest = mock(SolrQueryRequest.class);
     queryResponse = new SolrQueryResponse();
-    replaceNodeApi = new ReplaceNodeAPI(mockCoreContainer, mockQueryRequest, queryResponse);
+    replaceNodeApi = new ReplaceNode(mockCoreContainer, mockQueryRequest, queryResponse);
     messageCapturer = ArgumentCaptor.forClass(ZkNodeProps.class);
 
     when(mockCoreContainer.isZooKeeperAware()).thenReturn(true);
@@ -73,8 +74,7 @@ public class ReplaceNodeAPITest extends SolrTestCaseJ4 {
 
   @Test
   public void testCreatesValidOverseerMessage() throws Exception {
-    ReplaceNodeAPI.ReplaceNodeRequestBody requestBody =
-        new ReplaceNodeAPI.ReplaceNodeRequestBody("demoTargetNode", false, "async");
+    final var requestBody = new ReplaceNodeRequestBody("demoTargetNode", false, "async");
     replaceNodeApi.replaceNode("demoSourceNode", requestBody);
     verify(mockCommandRunner).runCollectionCommand(messageCapturer.capture(), any(), anyLong());
 
@@ -102,8 +102,7 @@ public class ReplaceNodeAPITest extends SolrTestCaseJ4 {
 
   @Test
   public void testOptionalValuesNotAddedToRemoteMessageIfNotProvided() throws Exception {
-    ReplaceNodeAPI.ReplaceNodeRequestBody requestBody =
-        new ReplaceNodeAPI.ReplaceNodeRequestBody("demoTargetNode", null, null);
+    final var requestBody = new ReplaceNodeRequestBody("demoTargetNode", null, null);
     replaceNodeApi.replaceNode("demoSourceNode", requestBody);
     verify(mockCommandRunner).runCollectionCommand(messageCapturer.capture(), any(), anyLong());
 
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/SyncShardAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/SyncShardAPITest.java
index 25ca5bb9cd1..23d3f8982a2 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/SyncShardAPITest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/SyncShardAPITest.java
@@ -21,7 +21,7 @@ import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.SolrException;
 import org.junit.Test;
 
-/** Unit tests for {@link SyncShardAPI} */
+/** Unit tests for {@link SyncShard} */
 public class SyncShardAPITest extends SolrTestCaseJ4 {
   @Test
   public void testReportsErrorIfCollectionNameMissing() {
@@ -29,7 +29,7 @@ public class SyncShardAPITest extends SolrTestCaseJ4 {
         expectThrows(
             SolrException.class,
             () -> {
-              final var api = new SyncShardAPI(null, null, null);
+              final var api = new SyncShard(null, null, null);
               api.syncShard(null, "someShard");
             });
 
@@ -43,7 +43,7 @@ public class SyncShardAPITest extends SolrTestCaseJ4 {
         expectThrows(
             SolrException.class,
             () -> {
-              final var api = new SyncShardAPI(null, null, null);
+              final var api = new SyncShard(null, null, null);
               api.syncShard("someCollection", null);
             });
 
diff --git a/solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java b/solr/core/src/test/org/apache/solr/security/GetPublicKeyTest.java
similarity index 84%
rename from solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java
rename to solr/core/src/test/org/apache/solr/security/GetPublicKeyTest.java
index f75b32b85d9..77bf4646a74 100644
--- a/solr/core/src/test/org/apache/solr/security/PublicKeyAPITest.java
+++ b/solr/core/src/test/org/apache/solr/security/GetPublicKeyTest.java
@@ -20,14 +20,14 @@ package org.apache.solr.security;
 import org.apache.solr.SolrTestCaseJ4;
 import org.junit.Test;
 
-/** Unit test for {@link PublicKeyAPI} */
-public class PublicKeyAPITest extends SolrTestCaseJ4 {
+/** Unit test for {@link GetPublicKey} */
+public class GetPublicKeyTest extends SolrTestCaseJ4 {
 
   @Test
   public void testRetrievesPublicKey() {
     final SolrNodeKeyPair nodeKeyPair = new SolrNodeKeyPair(null);
 
-    final PublicKeyAPI.PublicKeyResponse response = new PublicKeyAPI(nodeKeyPair).getPublicKey();
+    final var response = new GetPublicKey(nodeKeyPair).getPublicKey();
 
     assertEquals(nodeKeyPair.getKeyPair().getPublicKeyStr(), response.key);
   }
diff --git a/solr/solrj/src/resources/java-template/api.mustache b/solr/solrj/src/resources/java-template/api.mustache
index 84f7f06ebf3..93108eae513 100644
--- a/solr/solrj/src/resources/java-template/api.mustache
+++ b/solr/solrj/src/resources/java-template/api.mustache
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;