Как правильно выстраивать архитектуру метода контроллера на laravel?
Знакомая боль давняя.
Принцип следующий - 1) исходные данные 2) превращение в обьект, 3) проверка (валидация) обьекта, 4) передача обьекта в какое-то действие, 5) проверка результата (если нужна) и 6) генерация респонса.

Исходные данные. Смотрим что за фрейм. Лара. Умеет в инжекцию в метод контроллера и биндинг обьектов, если написать правильно. Можно сразу на вход писать MyActionRequest (сделать как в документации), в итоге первой строкой будет $myActionRequest->validate(), которая еще и выкинет 422 если что-то не так.

В чем минус ларавельного реквеста? Он представляет собой как бы ВЕСЬ реквест. Это тоже самое что передавать $_GET по программе, что немного странно.

Рекомендуется сделать свой обьект ДТО. Это по сути твой же реквест, только в нем поля все заранее описаны, голый класс без методов - одни свойства. После валидации ты свои данные из GET/POST/LaravelRequest ручками втыкаешь в поля ДТО и этот заполненный ДТО можешь дальше кидать как "точно проверенная пачка данных" и не боятся каждый раз "а что если что-то не так" - оно точно так, потому что ты уже проверял.

Попадать в какой-то другой класс ДТО может целиком если это кусок бизнес-части программы или по одному полю / массиву полей если это какой-то кусок ядра или глобалки. Пример ядро - класс Money, который переводит там валюты знач и позволяет их переключать. Туда ваш MyActionDTO будет странно кидать, а вот string $money  или PHPMoney/Money- пжалста. А вот в обьект например MyCatalog кинуть MyActionDto уже не так уже и плохо.

Вот это ваше действие может быть просто SQL запросом. Category::query()->find() это тоже действие, а почему нет. Рекомендуют конечно запросы выбрасывать в "тип классов" (папка, такая же как "модели") репозиторий чтоб потом когда поломается чего не везде править а в одном месте, но тут тоже, пока помните, что где можно не париться, и потом - увлечетесь обобщением поймаете прикол когда не понятно какую половину запроса в контроллер, а какую - в репозиторий. Стоит помнить что мы пишем задачу под задание, задание представляет собой контроллер, а остальной код ему помогает. Поэтому частности в экшон, похожести - в отдельный класс.

Ну так вот, это "действие" что-то вам вернет. Может это готовый к выводу контент, а может еще какие-то данные которые для вашего экшена может кривые. Вот проверяете "оно? или нет". Частный случай - ничего в базе не нашлось, это критично, и 404 или просто выплюнуть "нет записей" и 200? Проверили ифами, сделали респонсы - где 404ый, где двухсотый, где джсоновский, где иксмэльный, файл-довнлоадный или стримовый.

Потом пошли в Handler.php написали отлов исключений в целом по программе и их запись куда-то, в базу, в кеш или на удаленный логгер-анализатор, чтобы везде не писать try catch а просто писать throw и знать что оно "хоть как-то но отрисуется" и куда надо запишется.

public function destroy(Request $request, $id) // или MyDestroyRequest если юзер что-то еще передает
    {
        $request->validate(); // оно под капотом ещё и авторизацию проверит и 422 само выбросит

        // ... верно тут можно проверять политику, но в принципе если реквест наследовать то в ларе помоему их можно и в реквест код перенести и первый метод еще и это сделает
      
        $dto = new MyDestroyDto($request->valid()); // там внутри пишем что куда, хотя можно и здесь

        $myDomainService = new MyDomainService(); // или new MyCoreService();
        $status = $myDomainService->doSomething($dto); // или $result = $myCoreService->convert($dto->money);

        if ($status === 'ok') return $this->jsonDone('@api.msg.ok_message', 200); // вот метод jsonDone в базовом (абстрактном*) контроллере или, например, в трейте, напиши
        return $this->jsonFail('@api.err.error_message', 500); // собаки в тексте для наглядности, трехуровневый ключ перевода из файла lang, по собаке просто можно определить текст уже проходил через переводчик или нет. три уровня нужны, чтобы когда много переводов скапливается их можно было подключать частями. Первый уровень - где подключать (api, catalog, social), второй - что (msg, err, label, placeholder, title, h1)
    }
Да всё верно описал Григорий Васильков , согласен.

lexstile И у вас впринципе то тоже верно

Насчёт ДТО - согласен, но слишком муторно выходит везде ДТО пихать :(
Для ДТО - Spatie DataTransferObject

Я думаю в ПХП 8 простые данные позволительно именованными параметрами передавать
А если данные требуют доп. преобразования, то ДТО 

ответ обратно либо стандартным
response()->json()
либо свой обработчик, как Григорий Васильков предложил

вот вариант близкий к вашему написанию. В принципе я так и делаю
public function destroy($id)
    {        
        if (!ProjectPolicy::requestDelete($id)) {
            return response()->json(Response::getErrorMessage('Ошибка доступа'), 403);
        }

        $project = Project::find($id);
        if (!$project) {        
            return response()->json(Response::getErrorMessage('Проект не найден'), 404);
        }
// это вот в сервис класс.
        $project->delete();
        $this->deleteImageProject($project->logo);

        return response()->json(Response::getSuccessMessage('Вы успешно удалили проект'), 200);
    }