How to "add to cart" a product with custom input field and save it to Database?

To accomplish this you could use Magento built-in "additional_options" functionality so that you don't have to edit email template, admin order view, customer order view (etc) to display your custom options.

Github : https://github.com/srenon/Cloudways_Mymodule

/app/code/MagePal/CustomItemAddToCart/etc/events.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="checkout_cart_product_add_after">
        <observer name="magepal_Customitemaddtocart_checkout_cart_product_add_after" instance="MagePal\CustomItemAddToCart\Observer\CheckoutCartProductAddAfterObserver" />
    </event>
    <event name="sales_model_service_quote_submit_before">
        <observer name="magepal_Customitemaddtocart_sales_model_service_quote_submit_before" instance="MagePal\CustomItemAddToCart\Observer\SalesModelServiceQuoteSubmitBeforeObserver" />
    </event>
</config>

Add Option to Quote

/app/code/MagePal/CustomItemAddToCart/Observer/CheckoutCartProductAddAfterObserver.php

<?php

namespace MagePal\CustomItemAddToCart\Observer;

use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\App\RequestInterface;

class CheckoutCartProductAddAfterObserver implements ObserverInterface
{

    protected $_request;

    /**
     * @param RequestInterface $request
     */
    public function __construct(RequestInterface $request){
            $this->_request = $request;
    }

    /**
     * @param EventObserver $observer
     * @return void
     */
    public function execute(EventObserver $observer)
    {
        /* @var \Magento\Quote\Model\Quote\Item $item */
        $item = $observer->getQuoteItem();

        $additionalOptions = array();

        if ($additionalOption = $item->getOptionByCode('additional_options')){
            $additionalOptions = (array) unserialize($additionalOption->getValue());
        }

        $post = $this->_request->getParam('magepal');

        if(is_array($post)){
            foreach($post as $key => $value){
                if($key == '' || $value == ''){
                    continue;
                }

                $additionalOptions[] = [
                    'label' => $key,
                    'value' => $value
                ];
            }
        }

        if(count($additionalOptions) > 0){
            $item->addOption(array(
                'code' => 'additional_options',
                'value' => serialize($additionalOptions)
            ));
        }


        /* To Do */

        // Edit Cart - May need to remove option and read them
        // Pre-fill remarks on product edit pages


        /* Issues */

        // Create new cart item with identical option values will add a new line item, instead of increment the previous item qty

    }
}

Method #1 - Copying Option from quote_item to order_item using Observer See Magento 2 fieldset.xml; copy fields from quote to order

/app/code/MagePal/CustomItemAddToCart/Observer/SalesModelServiceQuoteSubmitBeforeObserver.php

<?php

namespace MagePal\CustomItemAddToCart\Observer;

use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Event\ObserverInterface;


class SalesModelServiceQuoteSubmitBeforeObserver implements ObserverInterface
{

    private $quoteItems = [];

    private $quote = null;
    private $order = null;

    /**
     * Add order information into GA block to render on checkout success pages
     *
     * @param EventObserver $observer
     * @return void
     */
    public function execute(EventObserver $observer)
    {

        $this->quote = $observer->getQuote();
        $this->order = $observer->getOrder();

        // can not find an equivalent event for sales_convert_quote_item_to_order_item


        /* @var  \Magento\Sales\Model\Order\Item $orderItem */
        foreach($this->order->getItems() as $orderItem){
            if(!$orderItem->getParentItemId() && $orderItem->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE){

                if($quoteItem = $this->getQuoteItemById($orderItem->getQuoteItemId())){
                    if ($additionalOptionsQuote = $quoteItem->getOptionByCode('additional_options')) {

                        //Todo
                        // - check to make sure element are not added twice
                        // - $additionalOptionsQuote - may not be an array
                        if($additionalOptionsOrder = $orderItem->getProductOptionByCode('additional_options')){
                            $additionalOptions = array_merge($additionalOptionsQuote, $additionalOptionsOrder);
                        }
                        else{
                            $additionalOptions = $additionalOptionsQuote;
                        }


                        if(count($additionalOptions) > 0){
                            $options = $orderItem->getProductOptions();
                            //As of Magento ~2.2, the unserialize() was discontinued in favor of json_encode.
                            // See https://devdocs.magento.com/guides/v2.4/ext-best-practices/tutorials/serialized-to-json-data-upgrade.html 
                            $options['additional_options'] = unserialize($additionalOptions->getValue());
                            $orderItem->setProductOptions($options);
                        }

                    }
                }
            }
        }

    }


    private function getQuoteItemById($id){
        if(empty($this->quoteItems)){
            /* @var  \Magento\Quote\Model\Quote\Item $item */
            foreach($this->quote->getItems() as $item){

                //filter out config/bundle etc product
                if(!$item->getParentItemId() && $item->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE){
                    $this->quoteItems[$item->getId()] = $item;
                }
            }
        }


        if(array_key_exists($id, $this->quoteItems)){
            return $this->quoteItems[$id];
        }

        return null;
    }
}

Method #2 - Copying Option from quote_item to order_item using Plugin

/app/code/MagePal/CustomItemAddToCart/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Quote\Model\Quote\Item\ToOrderItem">
        <plugin name="magepal_Customitemaddtocart_Sales_Quote_Item_ToOrderItem" type="MagePal\CustomItemAddToCart\Plugin\QuoteItemToOrderItemPlugin" />
    </type>
</config>

/app/code/MagePal/CustomItemAddToCart/Plugin/QuoteItemToOrderItemPlugin.php

<?php

namespace MagePal\CustomItemAddToCart\Plugin;

class QuoteItemToOrderItemPlugin
{

    public function aroundConvert(\Magento\Quote\Model\Quote\Item\ToOrderItem $subject, callable $proceed, $quoteItem, $data)
    {

        // get order item
        $orderItem = $proceed($quoteItem, $data);
        
        
        if(!$orderItem->getParentItemId() && $orderItem->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_SIMPLE){
            if ($additionalOptionsQuote = $quoteItem->getOptionByCode('additional_options')) {
                //To do
                // - check to make sure element are not added twice
                // - $additionalOptionsQuote - may not be an array
                if($additionalOptionsOrder = $orderItem->getProductOptionByCode('additional_options')){
                    $additionalOptions = array_merge($additionalOptionsQuote, $additionalOptionsOrder);
                }
                else{
                    $additionalOptions = $additionalOptionsQuote;
                }

                //As of Magento ~2.2, the unserialize() was discontinued in favor of json_encode.
                // See https://devdocs.magento.com/guides/v2.4/ext-best-practices/tutorials/serialized-to-json-data-upgrade.html 
                if(count($additionalOptions) > 0){
                    $options = $orderItem->getProductOptions();
                    $options['additional_options'] = unserialize($additionalOptions->getValue());
                    $orderItem->setProductOptions($options);
                }
            }
        }
        
        return $orderItem;
    }
}

Shopping cart and Admin order view page

Base off of Magento1 - Quote/order product item attribute based on user input