Magento 2 : How to create custom webapi?

First you need to create webapi.xml under your custom module Vendor\Module\etc\

<route url="/V1/custom/:categoryId/products" method="GET">
    <service class="Vendor\Module\Api\CategoryLinkManagementInterface" method="getAssignedProducts" />
    <resources>
        <resource ref="self"/>
    </resources>
</route>

– Route – This is the URL which will be used to call our API

https://{{MagentoBaseURL}}/index.php/rest/V1/custom/{{categoryId}}/products

– Service Class – This is the interface class of our API and the main method “getAssignedProducts” will be called with {{categoryId}} as the parameter

– Resources- This defines who has the permission to call this API. It could be anonymous (everyone) or self (customer) or specific admin user with specific permission for example Vendor_Module::custom which can be added in acl.xml

Lets now create the main interface file for your web API CategoryLinkManagementInterface.php under Vendor\Module\Api\ as specified in webapi.xml.

<?php
namespace Vendor\Module\Api;

/**
 * @api
 */
interface CategoryLinkManagementInterface
{
    /**
     * Get products assigned to a category
     *
     * @param int $categoryId
     * @return \Scommerce\Custom\Api\Data\CategoryProductLinkInterface[]
     */
    public function getAssignedProducts($categoryId);
}

In the above code, we have created an interface and define the main method i.e. getAssignedProducts as specified in webapi.xml. The other thing to notice here is the@return parameter which is the data interface \Vendor\Module\Api\Data\CategoryProductLinkInterface[]. Will explain what is data interface in our next step.

Based on the return parameter, let’s create our data interface CategoryProductLinkInterface.php under \Vendor\Module\Api\Data\

<?php
namespace Vendor\Module\Api\Data;

/**
 * @api
 */
interface CategoryProductLinkInterface
{
    /**
     * @return string|null
     */
    public function getSku();

    /**
     * @param string $sku
     * @return $this
     */
    public function setSku($sku);

    /**
     * @return string|null
     */
    public function getName();

    /**
     * @param string $name
     * @return $this
     */
    public function setName($name);

    /**
     * @return float|null
     */
    public function getPrice();

    /**
     * @param float $price
     * @return $this
     */
    public function setPrice($price);

    /**
     * @return int|null
     */
    public function getPosition();

    /**
     * @param int $position
     * @return $this
     */
    public function setPosition($position);

    /**
     * @return string|null
     */
    public function getCategoryDescription();

    /**
     * @param string $description
     * @return $this
     */
    public function setCategoryDescription($description);
}

The above data interface class allows you define the response/output of our API request, so as you can see this API will return sku, name, price, position and category description as an output.

Now your interface files are created, let’s create our model classes where we can put the actual business logic, to do so we would need to specify this in our di.xml file under \Vendor\Module\etc\

<config ...>
    <preference for="Vendor\Module\Api\CategoryLinkManagementInterface" type="Vendor\Module\Model\CategoryLinkManagement" />
    <preference for="Vendor\Module\Api\Data\CategoryProductLinkInterface" type="Vendor\Module\Model\CategoryProductLink" />
</config>

In the above step, we have specified which model classes will be created against our interfaces to add our business logic.

Let’s create our first model class CategoryLinkManagement.php under Vendor\Module\Model\ as specified in di.xml

<?php
    namespace Vendor\Module\Model;

/**
 * Class CategoryLinkManagement
 */
class CategoryLinkManagement implements \Vendor\Module\Api\CategoryLinkManagementInterface
{
    /**
     * @var \Magento\Catalog\Api\CategoryRepositoryInterface
     */
    protected $categoryRepository;

    /**
     * @var \Vendor\Module\Api\Data\CategoryProductLinkInterfaceFactory
     */
    protected $productLinkFactory;

    /**
     * CategoryLinkManagement constructor.
     *
     * @param \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository
     * @param \Scommerce\Custom\Api\Data\CategoryProductLinkInterfaceFactory $productLinkFactory
     */
    public function __construct(
        \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository,
        \Scommerce\Custom\Api\Data\CategoryProductLinkInterfaceFactory $productLinkFactory
    ) {
        $this->categoryRepository = $categoryRepository;
        $this->productLinkFactory = $productLinkFactory;
    }

    /**
     * {@inheritdoc}
     */
    public function getAssignedProducts($categoryId)
    {
        $category = $this->categoryRepository->get($categoryId);
        if (!$category->getIsActive()) {
            return [[
                'error' => true,
                'error_desc' => 'Category is disabled'
            ]];
        }
        $categoryDesc = $category->getDescription();

        /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $products */
        $products = $category->getProductCollection()
            ->addFieldToSelect('position')
            ->addFieldToSelect('name')
            ->addFieldToSelect('price');

        /** @var \Scommerce\Custom\Api\Data\CategoryProductLinkInterface[] $links */
        $links = [];

        /** @var \Magento\Catalog\Model\Product $product */
        foreach ($products->getItems() as $product) {
            /** @var \Scommerce\Custom\Api\Data\CategoryProductLinkInterface $link */
            $link = $this->productLinkFactory->create();
            $link->setSku($product->getSku())
                ->setName($product->getName())
                ->setPrice($product->getFinalPrice())
                ->setPosition($product->getData('cat_index_position'))
                ->setCategoryDescription($categoryDesc);
            $links[] = $link;
        }

        return $links;
    }
}

Lets now create our second model class CategoryProductLink.php under Vendor\Module\Model\ as specified in di.xml

<?php
namespace Vendor\Module\Model;

/**
 * @codeCoverageIgnore
 */
class CategoryProductLink implements \Vendor\Module\Api\Data\CategoryProductLinkInterface
{
    /**#@+
     * Constant for confirmation status
     */
    const KEY_SKU                   = 'sku';
    const KEY_NAME                  = 'name';
    const KEY_PRICE                 = 'price';
    const KEY_CATEGORY_DESC         = 'category_description';
    const KEY_POSITION              = 'position';
    /**#@-*/

    /**
     * {@inheritdoc}
     */
    public function getSku()
    {
        return $this->_get(self::KEY_SKU);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return $this->_get(self::KEY_NAME);
    }

    /**
     * {@inheritdoc}
     */
    public function getPosition()
    {
        return $this->_get(self::KEY_POSITION);
    }

    /**
     * {@inheritdoc}
     */
    public function getPrice()
    {
        return $this->_get(self::KEY_PRICE);
    }

    /**
     * {@inheritdoc}
     */
    public function getCategoryDescription()
    {
        return $this->_get(self::KEY_CATEGORY_DESC);
    }

    /**
     * @param string $sku
     * @return $this
     */
    public function setSku($sku)
    {
        return $this->setData(self::KEY_SKU, $sku);
    }

    /**
     * @param string $name
     * @return $this
     */
    public function setName($name)
    {
        return $this->setData(self::KEY_NAME, $name);
    }

    /**
     * @param int $position
     * @return $this
     */
    public function setPosition($position)
    {
        return $this->setData(self::KEY_POSITION, $position);
    }

    /**
     * @param float $price
     * @return $this
     */
    public function setPrice($price)
    {
        return $this->setData(self::KEY_PRICE, $price);
    }

    /**
     * @param string $description
     * @return $this
     */
    public function setCategoryDescription($description)
    {
        return $this->setData(self::KEY_CATEGORY_DESC, $description);
    }

}

I hope this will help