diff --git a/Neos.Flow/Classes/Annotations/Route.php b/Neos.Flow/Classes/Annotations/Route.php
new file mode 100644
index 0000000000..4d9e8d86d3
--- /dev/null
+++ b/Neos.Flow/Classes/Annotations/Route.php
@@ -0,0 +1,67 @@
+<?php
+declare(strict_types=1);
+
+namespace Neos\Flow\Annotations;
+
+/*
+ * This file is part of the Neos.Flow package.
+ *
+ * (c) Contributors of the Neos Project - www.neos.io
+ *
+ * This package is Open Source Software. For the full copyright and license
+ * information, please view the LICENSE file which was distributed with this
+ * source code.
+ */
+
+use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
+
+/**
+ * Adds a route configuration to a method
+ *
+ * This is a convenient way to add routes in project code
+ * but should not be used in libraries/packages that shall be
+ * configured for different use cases.
+ *
+ * @Annotation
+ * @NamedArgumentConstructor
+ * @Target({"METHOD"})
+ */
+#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
+final readonly class Route
+{
+    /**
+     * Magic route values cannot be set as default nor be contained as segments like `{\@action}` or `{\@controller}` in the uriPattern.
+     * The magic route value `\@format` is allowed if necessary.
+     */
+    private const PRESERVED_DEFAULTS = ['@package', '@subpackage', '@controller', '@action'];
+
+    /**
+     * @param string $uriPattern The uri-pattern for the route without leading '/'. Might contain route values in the form of `path/{foo}`
+     * @param string $name The suffix of the route name as shown in `route:list` (defaults to the action name: "My.Package :: Site :: index")
+     * @param array $httpMethods List of uppercase http verbs like 'GET', 'POST', 'PUT', 'DELETE', if not specified any request method will be matched
+     * @param array $defaults Values to set for this route
+     */
+    public function __construct(
+        public string $uriPattern,
+        public string $name = '',
+        public array $httpMethods = [],
+        public array $defaults = [],
+    ) {
+        if ($uriPattern === '' || str_starts_with($uriPattern, '/')) {
+            throw new \DomainException(sprintf('Uri pattern must not be empty or begin with a slash: "%s"', $uriPattern), 1711529592);
+        }
+        foreach ($httpMethods as $httpMethod) {
+            if ($httpMethod === '' || ctype_lower($httpMethod)) {
+                throw new \DomainException(sprintf('Http method must not be empty or be lower case: "%s"', $httpMethod), 1711530485);
+            }
+        }
+        foreach (self::PRESERVED_DEFAULTS as $preservedDefaultName) {
+            if (str_contains($uriPattern, sprintf('{%s}', $preservedDefaultName))) {
+                throw new \DomainException(sprintf('It is not allowed to use "%s" in the uri pattern "%s"', $preservedDefaultName, $uriPattern), 1711129634);
+            }
+            if (array_key_exists($preservedDefaultName, $defaults)) {
+                throw new \DomainException(sprintf('It is not allowed to override "%s" as default', $preservedDefaultName), 1711129638);
+            }
+        }
+    }
+}
diff --git a/Neos.Flow/Classes/Configuration/Loader/RoutesLoader.php b/Neos.Flow/Classes/Configuration/Loader/RoutesLoader.php
index a02a5c152f..c3f5fb8da4 100644
--- a/Neos.Flow/Classes/Configuration/Loader/RoutesLoader.php
+++ b/Neos.Flow/Classes/Configuration/Loader/RoutesLoader.php
@@ -84,12 +84,20 @@ public function load(array $packages, ApplicationContext $context): array
     protected function includeSubRoutesFromSettings(array $routeDefinitions, array $routeSettings): array
     {
         $sortedRouteSettings = (new PositionalArraySorter($routeSettings))->toArray();
-        foreach ($sortedRouteSettings as $packageKey => $routeFromSettings) {
+        foreach ($sortedRouteSettings as $configurationKey => $routeFromSettings) {
             if ($routeFromSettings === false) {
                 continue;
             }
-            $subRoutesName = $packageKey . 'SubRoutes';
-            $subRoutesConfiguration = ['package' => $packageKey];
+            if (isset($routeFromSettings['providerFactory'])) {
+                $routeDefinitions[] = [
+                    'name' => $configurationKey,
+                    'providerFactory' => $routeFromSettings['providerFactory'],
+                    'providerOptions' => $routeFromSettings['providerOptions'] ?? [],
+                ];
+                continue;
+            }
+            $subRoutesName = $configurationKey . 'SubRoutes';
+            $subRoutesConfiguration = ['package' => $configurationKey];
             if (isset($routeFromSettings['variables'])) {
                 $subRoutesConfiguration['variables'] = $routeFromSettings['variables'];
             }
@@ -97,7 +105,7 @@ protected function includeSubRoutesFromSettings(array $routeDefinitions, array $
                 $subRoutesConfiguration['suffix'] = $routeFromSettings['suffix'];
             }
             $routeDefinitions[] = [
-                'name' => $packageKey,
+                'name' => $configurationKey,
                 'uriPattern' => '<' . $subRoutesName . '>',
                 'subRoutes' => [
                     $subRoutesName => $subRoutesConfiguration
@@ -128,6 +136,10 @@ protected function mergeRoutesWithSubRoutes(array $packages, ApplicationContext
             }
             $mergedSubRoutesConfiguration = [$routeConfiguration];
             foreach ($routeConfiguration['subRoutes'] as $subRouteKey => $subRouteOptions) {
+                if (isset($subRouteOptions['providerFactory'])) {
+                    $mergedRoutesConfiguration[] = $subRouteOptions;
+                    continue;
+                }
                 if (!isset($subRouteOptions['package'])) {
                     throw new ParseErrorException(sprintf('Missing package configuration for SubRoute in Route "%s".', ($routeConfiguration['name'] ?? 'unnamed Route')), 1318414040);
                 }
diff --git a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php
new file mode 100644
index 0000000000..df77ab0840
--- /dev/null
+++ b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProvider.php
@@ -0,0 +1,149 @@
+<?php
+
+/*
+ * This file is part of the Neos.Flow package.
+ *
+ * (c) Contributors of the Neos Project - www.neos.io
+ *
+ * This package is Open Source Software. For the full copyright and license
+ * information, please view the LICENSE file which was distributed with this
+ * source code.
+ */
+
+declare(strict_types=1);
+
+namespace Neos\Flow\Mvc\Routing;
+
+use Neos\Flow\Mvc\Controller\ActionController;
+use Neos\Flow\Mvc\Exception\InvalidActionNameException;
+use Neos\Flow\Mvc\Routing\Exception\InvalidControllerException;
+use Neos\Flow\ObjectManagement\ObjectManagerInterface;
+use Neos\Flow\Reflection\ReflectionService;
+use Neos\Flow\Annotations as Flow;
+use Neos\Utility\Arrays;
+
+/**
+ * Allows to annotate controller methods with route configurations
+ *
+ * Internal implementation:
+ * -----------------------
+ *
+ * Flows routing configuration is declared via \@package, \@subpackage, \@controller and \@action
+ * The first three options will resolve to a fully qualified class name {@see \Neos\Flow\Mvc\ActionRequest::getControllerObjectName()}
+ * which is instantiated in the dispatcher {@see \Neos\Flow\Mvc\Dispatcher::dispatch()}
+ *
+ * The latter \@action option will be treated internally by each controller. From the perspective of the dispatcher \@action is just another routing value.
+ * By convention during processRequest in the default ActionController {@see \ActionController::resolveActionMethodName()} will be used
+ * to concatenate the "Action" suffix to the action name
+ * and {@see ActionController::callActionMethod()} will invoke the method internally with prepared method arguments.
+ *
+ * Creating routes by annotation must make a few assumptions to work:
+ *
+ * 1. As not every FQ class name is representable via the routing configuration (e.g. the class has to end with "Controller"),
+ * only classes can be annotated that reside in a correct location and have the correct suffix.
+ * Otherwise, an exception will be thrown as the class is not discoverable by the dispatcher.
+ *
+ * 2. As the ActionController requires a little magic and is the main use case we currently only support this controller.
+ * For that reason it is validated that the annotation is inside an ActionController and the method ends with "Action".
+ * The routing value with the suffix trimmed will be generated:
+ *
+ *     class MyThingController extends ActionController
+ *     {
+ *         #[Flow\Route(path: 'foo')]
+ *         public function someAction()
+ *         {
+ *         }
+ *     }
+ *
+ * The example will genrate the configuration:
+ *
+ *     \@package My.Package
+ *     \@controller MyThing
+ *     \@action some
+ *
+ * TODO for a future scope of `Flow\Action` see {@link https://github.com/neos/flow-development-collection/issues/3335}
+ */
+final class AttributeRoutesProvider implements RoutesProviderInterface
+{
+    /**
+     * @param array<string> $classNames
+     */
+    public function __construct(
+        public readonly ReflectionService $reflectionService,
+        public readonly ObjectManagerInterface $objectManager,
+        public readonly array $classNames,
+    ) {
+    }
+
+    public function getRoutes(): Routes
+    {
+        $routes = [];
+        $annotatedClasses = $this->reflectionService->getClassesContainingMethodsAnnotatedWith(Flow\Route::class);
+
+        foreach ($annotatedClasses as $className) {
+            $includeClassName = false;
+            foreach ($this->classNames as $classNamePattern) {
+                if (fnmatch($classNamePattern, $className, FNM_NOESCAPE)) {
+                    $includeClassName = true;
+                    break;
+                }
+            }
+            if (!$includeClassName) {
+                continue;
+            }
+
+            if (!in_array(ActionController::class, class_parents($className), true)) {
+                throw new InvalidControllerException('TODO: Currently #[Flow\Route] is only supported for ActionController. See https://github.com/neos/flow-development-collection/issues/3335.');
+            }
+
+            $controllerObjectName = $this->objectManager->getCaseSensitiveObjectName($className);
+            $controllerPackageKey = $this->objectManager->getPackageKeyByObjectName($controllerObjectName);
+            $controllerPackageNamespace = str_replace('.', '\\', $controllerPackageKey);
+            if (!str_ends_with($className, 'Controller')) {
+                throw new InvalidControllerException('Only for controller classes');
+            }
+
+            $localClassName = substr($className, strlen($controllerPackageNamespace) + 1);
+
+            if (str_starts_with($localClassName, 'Controller\\')) {
+                $controllerName = substr($localClassName, 11);
+                $subPackage = null;
+            } elseif (str_contains($localClassName, '\\Controller\\')) {
+                list($subPackage, $controllerName) = explode('\\Controller\\', $localClassName);
+            } else {
+                throw new InvalidControllerException('Unknown controller pattern');
+            }
+
+            $annotatedMethods = $this->reflectionService->getMethodsAnnotatedWith($className, Flow\Route::class);
+            foreach ($annotatedMethods as $methodName) {
+                if (!str_ends_with($methodName, 'Action')) {
+                    throw new InvalidActionNameException('Only for action methods');
+                }
+                $annotations = $this->reflectionService->getMethodAnnotations($className, $methodName, Flow\Route::class);
+                foreach ($annotations as $annotation) {
+                    if ($annotation instanceof Flow\Route) {
+                        $controller = substr($controllerName, 0, -10);
+                        $action = substr($methodName, 0, -6);
+                        $configuration = [
+                            'name' => $controllerPackageKey . ' :: ' . $controller . ' :: ' . ($annotation->name ?: $action),
+                            'uriPattern' => $annotation->uriPattern,
+                            'httpMethods' => $annotation->httpMethods,
+                            'defaults' => Arrays::arrayMergeRecursiveOverrule(
+                                [
+                                    '@package' => $controllerPackageKey,
+                                    '@subpackage' => $subPackage,
+                                    '@controller' => $controller,
+                                    '@action' => $action,
+                                    '@format' => 'html'
+                                ],
+                                $annotation->defaults ?? []
+                            )
+                        ];
+                        $routes[] = Route::fromConfiguration($configuration);
+                    }
+                }
+            }
+        }
+        return Routes::create(...$routes);
+    }
+}
diff --git a/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProviderFactory.php b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProviderFactory.php
new file mode 100644
index 0000000000..ff6aa1b81c
--- /dev/null
+++ b/Neos.Flow/Classes/Mvc/Routing/AttributeRoutesProviderFactory.php
@@ -0,0 +1,39 @@
+<?php
+
+/*
+ * This file is part of the Neos.Flow package.
+ *
+ * (c) Contributors of the Neos Project - www.neos.io
+ *
+ * This package is Open Source Software. For the full copyright and license
+ * information, please view the LICENSE file which was distributed with this
+ * source code.
+ */
+
+declare(strict_types=1);
+
+namespace Neos\Flow\Mvc\Routing;
+
+use Neos\Flow\ObjectManagement\ObjectManagerInterface;
+use Neos\Flow\Reflection\ReflectionService;
+
+class AttributeRoutesProviderFactory implements RoutesProviderFactoryInterface
+{
+    public function __construct(
+        public readonly ReflectionService $reflectionService,
+        public readonly ObjectManagerInterface $objectManager,
+    ) {
+    }
+
+    /**
+     * @param array<string, mixed> $options
+     */
+    public function createRoutesProvider(array $options): RoutesProviderInterface
+    {
+        return new AttributeRoutesProvider(
+            $this->reflectionService,
+            $this->objectManager,
+            $options['classNames'] ?? [],
+        );
+    }
+}
diff --git a/Neos.Flow/Classes/Mvc/Routing/ConfigurationRoutesProvider.php b/Neos.Flow/Classes/Mvc/Routing/ConfigurationRoutesProvider.php
index f876008e8c..63d2d366f8 100644
--- a/Neos.Flow/Classes/Mvc/Routing/ConfigurationRoutesProvider.php
+++ b/Neos.Flow/Classes/Mvc/Routing/ConfigurationRoutesProvider.php
@@ -6,22 +6,36 @@
 
 use Neos\Flow\Annotations as Flow;
 use Neos\Flow\Configuration\ConfigurationManager;
+use Neos\Flow\ObjectManagement\ObjectManagerInterface;
 
 /**
  * @Flow\Scope("singleton")
  */
 final class ConfigurationRoutesProvider implements RoutesProviderInterface
 {
-    private ConfigurationManager $configurationManager;
-
     public function __construct(
-        ConfigurationManager $configurationManager
+        private ConfigurationManager $configurationManager,
+        private ObjectManagerInterface $objectManager,
     ) {
-        $this->configurationManager = $configurationManager;
     }
 
     public function getRoutes(): Routes
     {
-        return Routes::fromConfiguration($this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_ROUTES));
+        $routes = [];
+        foreach ($this->configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_ROUTES) as $routeConfiguration) {
+            if (isset($routeConfiguration['providerFactory'])) {
+                $providerFactory = $this->objectManager->get($routeConfiguration['providerFactory']);
+                if (!$providerFactory instanceof RoutesProviderFactoryInterface) {
+                    throw new \InvalidArgumentException(sprintf('The configured route providerFactory "%s" does not implement the "%s"', $routeConfiguration['providerFactory'], RoutesProviderFactoryInterface::class), 1710784630);
+                }
+                $provider = $providerFactory->createRoutesProvider($routeConfiguration['providerOptions'] ?? []);
+                foreach ($provider->getRoutes() as $route) {
+                    $routes[] = $route;
+                }
+            } else {
+                $routes[] = Route::fromConfiguration($routeConfiguration);
+            }
+        }
+        return Routes::create(...$routes);
     }
 }
diff --git a/Neos.Flow/Classes/Mvc/Routing/RoutesProviderFactoryInterface.php b/Neos.Flow/Classes/Mvc/Routing/RoutesProviderFactoryInterface.php
new file mode 100644
index 0000000000..642d984c3b
--- /dev/null
+++ b/Neos.Flow/Classes/Mvc/Routing/RoutesProviderFactoryInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+/*
+ * This file is part of the Neos.Flow package.
+ *
+ * (c) Contributors of the Neos Project - www.neos.io
+ *
+ * This package is Open Source Software. For the full copyright and license
+ * information, please view the LICENSE file which was distributed with this
+ * source code.
+ */
+
+declare(strict_types=1);
+
+namespace Neos\Flow\Mvc\Routing;
+
+interface RoutesProviderFactoryInterface
+{
+    /**
+     * @param array<string, mixed> $options
+     */
+    public function createRoutesProvider(array $options): RoutesProviderInterface;
+}
diff --git a/Neos.Flow/Classes/Mvc/Routing/RoutesProviderInterface.php b/Neos.Flow/Classes/Mvc/Routing/RoutesProviderInterface.php
index 5cd3f98ae6..61221fb15c 100644
--- a/Neos.Flow/Classes/Mvc/Routing/RoutesProviderInterface.php
+++ b/Neos.Flow/Classes/Mvc/Routing/RoutesProviderInterface.php
@@ -1,5 +1,15 @@
 <?php
 
+/*
+ * This file is part of the Neos.Flow package.
+ *
+ * (c) Contributors of the Neos Project - www.neos.io
+ *
+ * This package is Open Source Software. For the full copyright and license
+ * information, please view the LICENSE file which was distributed with this
+ * source code.
+ */
+
 declare(strict_types=1);
 
 namespace Neos\Flow\Mvc\Routing;
diff --git a/Neos.Flow/Classes/Package.php b/Neos.Flow/Classes/Package.php
index 94405c7a34..49808ac35c 100644
--- a/Neos.Flow/Classes/Package.php
+++ b/Neos.Flow/Classes/Package.php
@@ -11,6 +11,7 @@
  * source code.
  */
 
+use Neos\Flow\Annotations\Route;
 use Neos\Flow\Cache\AnnotationsCacheFlusher;
 use Neos\Flow\Configuration\Loader\AppendLoader;
 use Neos\Flow\Configuration\Source\YamlSource;
@@ -158,6 +159,7 @@ public function boot(Core\Bootstrap $bootstrap)
 
         $dispatcher->connect(Proxy\Compiler::class, 'compiledClasses', function (array $classNames) use ($bootstrap) {
             $annotationsCacheFlusher = $bootstrap->getObjectManager()->get(AnnotationsCacheFlusher::class);
+            $annotationsCacheFlusher->registerAnnotation(Route::class, ['Flow_Mvc_Routing_Route', 'Flow_Mvc_Routing_Resolve']);
             $annotationsCacheFlusher->flushConfigurationCachesByCompiledClass($classNames);
         });
     }
diff --git a/Neos.Flow/Classes/Reflection/ReflectionService.php b/Neos.Flow/Classes/Reflection/ReflectionService.php
index 72bb218fe9..1a94d10339 100644
--- a/Neos.Flow/Classes/Reflection/ReflectionService.php
+++ b/Neos.Flow/Classes/Reflection/ReflectionService.php
@@ -1273,7 +1273,7 @@ protected function reflectClassMethod(string $className, MethodReflection $metho
             if (!isset($this->classesByMethodAnnotations[$annotationClassName][$className])) {
                 $this->classesByMethodAnnotations[$annotationClassName][$className] = [];
             }
-            $this->classesByMethodAnnotations[$annotationClassName][$className][] = $methodName;
+            $this->classesByMethodAnnotations[$annotationClassName][$className][$methodName] = $methodName;
         }
 
         $returnType = $method->getDeclaredReturnType();
diff --git a/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Routing.rst b/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Routing.rst
index 83f72a1988..7821c8aac8 100644
--- a/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Routing.rst
+++ b/Neos.Flow/Documentation/TheDefinitiveGuide/PartIII/Routing.rst
@@ -729,10 +729,67 @@ two more options you can use:
 With ``suffix`` you can specify a custom filename suffix for the SubRoute. The ``variables`` option allows you to
 specify placeholders in the SubRoutes (see `Nested Subroutes`_).
 
+It also is possible to specify a `providerFactory` and (optional) `providerOptions` to generate the subroutes via the
+`Neos\Flow\Mvc\Routing\RoutesProviderFactoryInterface` and the ``Neos\Flow\Mvc\Routing\RoutesProviderInterface`.
+
+.. code-block:: yaml
+
+  Neos:
+    Flow:
+      mvc:
+        routes:
+          Vendor.Example.attributes:
+            position: 'before Neos.Neos'
+            providerFactory: \Neos\Flow\Mvc\Routing\AttributeRoutesProviderFactory
+            providerOptions:
+              classNames:
+                - Vendor\Example\Controller\ExampleController
+
 .. tip::
 
 	You can use the ``flow:routing:list`` command to list all routes which are currently active, see `CLI`_
 
+Subroutes from Annotations
+--------------------------
+
+The ``Flow\Route`` attribute allows to define routes directly on the affected method.
+(Currently only ActionController are supported https://github.com/neos/flow-development-collection/issues/3335)
+
+.. code-block:: php
+
+  use Neos\Flow\Mvc\Controller\ActionController;
+  use Neos\Flow\Annotations as Flow;
+
+  class ExampleController extends ActionController
+  {
+      #[Flow\Route(uriPattern:'my/path', httpMethods: ['GET'])]
+      public function someAction(): void
+      {
+      }
+
+      #[Flow\Route(uriPattern:'my/other/b-path', defaults: ['test' => 'b'])]
+      #[Flow\Route(uriPattern:'my/other/c-path', defaults: ['test' => 'c'])]
+      public function otherAction(string $test): void
+      {
+      }
+  }
+
+To find the annotation and tp specify the order of routes this has to be used together with the
+`\Neos\Flow\Mvc\Routing\AttributeRoutesProviderFactory` as `providerFactory` in  Setting `Neos.Flow.mvc.routes`
+
+.. code-block:: yaml
+
+  Neos:
+    Flow:
+      mvc:
+        routes:
+          Vendor.Example.attributes:
+            position: 'before Neos.Neos'
+            providerFactory: \Neos\Flow\Mvc\Routing\AttributeRoutesProviderFactory
+            providerOptions:
+              classNames:
+                - Vendor\Example\Controller\*
+
 Route Loading Order and the Flow Application Context
 ====================================================
 
diff --git a/Neos.Flow/Tests/Unit/Mvc/Routing/AttributeRoutesProviderTest.php b/Neos.Flow/Tests/Unit/Mvc/Routing/AttributeRoutesProviderTest.php
new file mode 100644
index 0000000000..4ee9cd3459
--- /dev/null
+++ b/Neos.Flow/Tests/Unit/Mvc/Routing/AttributeRoutesProviderTest.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Neos\Flow\Tests\Unit\Mvc\Routing;
+
+/*
+ * This file is part of the Neos.Flow package.
+ *
+ * (c) Contributors of the Neos Project - www.neos.io
+ *
+ * This package is Open Source Software. For the full copyright and license
+ * information, please view the LICENSE file which was distributed with this
+ * source code.
+ */
+
+use Neos\Flow\Mvc\Routing;
+use Neos\Flow\Mvc\Routing\Route;
+use Neos\Flow\Mvc\Routing\Routes;
+use Neos\Flow\ObjectManagement\ObjectManagerInterface;
+use Neos\Flow\Reflection\ReflectionService;
+use Neos\Flow\Tests\UnitTestCase;
+use PHPUnit\Framework\MockObject\MockObject;
+use Neos\Flow\Annotations as Flow;
+
+/**
+ * Testcase for the MVC Web Routing Routes Class
+ */
+class AttributeRoutesProviderTest extends UnitTestCase
+{
+    private ReflectionService|MockObject $mockReflectionService;
+    private ObjectManagerInterface|MockObject $mockObjectManager;
+    private Routing\AttributeRoutesProvider $annotationRoutesProvider;
+
+    public function setUp(): void
+    {
+        $this->mockReflectionService = $this->createMock(ReflectionService::class);
+        $this->mockObjectManager = $this->createMock(ObjectManagerInterface::class);
+
+        $this->annotationRoutesProvider = new Routing\AttributeRoutesProvider(
+            $this->mockReflectionService,
+            $this->mockObjectManager,
+            ['Vendor\\Example\\Controller\\*']
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function noAnnotationsYieldEmptyRoutes(): void
+    {
+        $this->mockReflectionService->expects($this->once())
+            ->method('getClassesContainingMethodsAnnotatedWith')
+            ->with(\Neos\Flow\Annotations\Route::class)
+            ->willReturn([]);
+
+        $routes = $this->annotationRoutesProvider->getRoutes();
+        $this->assertEquals(Routes::empty(), $routes);
+    }
+
+    /**
+     * @test
+     */
+    public function routesFromAnnotationAreCreatedWhenClassNamesMatch(): void
+    {
+        $exampleFqnControllerName = 'Vendor\\Example\\Controller\\ExampleController';
+        eval('
+        namespace Vendor\Example\Controller;
+        class ExampleController extends \Neos\Flow\Mvc\Controller\ActionController {
+        }'
+        );
+
+        $this->mockReflectionService->expects($this->once())
+            ->method('getClassesContainingMethodsAnnotatedWith')
+            ->with(Flow\Route::class)
+            ->willReturn([$exampleFqnControllerName]);
+
+        $this->mockReflectionService->expects($this->once())
+            ->method('getMethodsAnnotatedWith')
+            ->with($exampleFqnControllerName, Flow\Route::class)
+            ->willReturn(['specialAction']);
+
+        $this->mockReflectionService->expects($this->once())
+            ->method('getMethodAnnotations')
+            ->with($exampleFqnControllerName, 'specialAction', Flow\Route::class)
+            ->willReturn([
+                new Flow\Route(uriPattern: 'my/path'),
+                new Flow\Route(
+                    uriPattern: 'my/other/path',
+                    name: 'specialRoute',
+                    httpMethods: ['GET', 'POST'],
+                    defaults: ['test' => 'foo']
+                )
+            ]);
+
+        $this->mockObjectManager->expects($this->once())
+            ->method('getCaseSensitiveObjectName')
+            ->with($exampleFqnControllerName)
+            ->willReturn($exampleFqnControllerName);
+
+        $this->mockObjectManager->expects($this->once())
+            ->method('getPackageKeyByObjectName')
+            ->with($exampleFqnControllerName)
+            ->willReturn('Vendor.Example');
+
+        $expectedRoute1 = new Route();
+        $expectedRoute1->setName('Vendor.Example :: Example :: special');
+        $expectedRoute1->setUriPattern('my/path');
+        $expectedRoute1->setDefaults([
+            '@package' => 'Vendor.Example',
+            '@subpackage' => null,
+            '@controller' => 'Example',
+            '@action' => 'special',
+            '@format' => 'html',
+        ]);
+
+        $expectedRoute2 = new Route();
+        $expectedRoute2->setName('Vendor.Example :: Example :: specialRoute');
+        $expectedRoute2->setUriPattern('my/other/path');
+        $expectedRoute2->setDefaults([
+            '@package' => 'Vendor.Example',
+            '@subpackage' => null,
+            '@controller' => 'Example',
+            '@action' => 'special',
+            '@format' => 'html',
+            'test' => 'foo',
+        ]);
+        $expectedRoute2->setHttpMethods(['GET', 'POST']);
+
+        $this->assertEquals(
+            Routes::create($expectedRoute1, $expectedRoute2),
+            $this->annotationRoutesProvider->getRoutes()
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function annotationsOutsideClassNamesAreIgnored(): void
+    {
+        $this->mockReflectionService->expects($this->once())
+            ->method('getClassesContainingMethodsAnnotatedWith')
+            ->with(Flow\Route::class)
+            ->willReturn(['Vendor\Other\Controller\ExampleController']);
+
+        $this->assertEquals(Routes::empty(), $this->annotationRoutesProvider->getRoutes());
+    }
+}
diff --git a/Neos.Flow/Tests/Unit/Mvc/Routing/ConfigurationRoutesProviderTest.php b/Neos.Flow/Tests/Unit/Mvc/Routing/ConfigurationRoutesProviderTest.php
index 08651da9ad..190c10037e 100644
--- a/Neos.Flow/Tests/Unit/Mvc/Routing/ConfigurationRoutesProviderTest.php
+++ b/Neos.Flow/Tests/Unit/Mvc/Routing/ConfigurationRoutesProviderTest.php
@@ -15,6 +15,7 @@
 use Neos\Flow\Mvc\Routing;
 use Neos\Flow\Mvc\Routing\Route;
 use Neos\Flow\Mvc\Routing\Routes;
+use Neos\Flow\ObjectManagement\ObjectManagerInterface;
 use Neos\Flow\Tests\UnitTestCase;
 
 /**
@@ -27,9 +28,10 @@ class ConfigurationRoutesProviderTest extends UnitTestCase
      */
     public function configurationManagerIsNotCalledInConstructor(): void
     {
+        $mockObjectManager = $this->createMock(ObjectManagerInterface::class);
         $mockConfigurationManager = $this->createMock(ConfigurationManager::class);
         $mockConfigurationManager->expects($this->never())->method('getConfiguration');
-        $configurationRoutesProvider = new Routing\ConfigurationRoutesProvider($mockConfigurationManager);
+        $configurationRoutesProvider = new Routing\ConfigurationRoutesProvider($mockConfigurationManager, $mockObjectManager);
         $this->assertInstanceOf(Routing\ConfigurationRoutesProvider::class, $configurationRoutesProvider);
     }
 
@@ -53,6 +55,7 @@ public function configurationFomConfigurationManagerIsHandled(): void
             ],
         ];
 
+        $mockObjectManager = $this->createMock(ObjectManagerInterface::class);
         $mockConfigurationManager = $this->createMock(ConfigurationManager::class);
         $mockConfigurationManager->expects($this->once())->method('getConfiguration')->with(ConfigurationManager::CONFIGURATION_TYPE_ROUTES)->willReturn($configuration);
 
@@ -71,7 +74,70 @@ public function configurationFomConfigurationManagerIsHandled(): void
 
         $expectedRoutes = Routes::create($expectedRoute1, $expectedRoute2);
 
-        $configurationRoutesProvider = new Routing\ConfigurationRoutesProvider($mockConfigurationManager);
+        $configurationRoutesProvider = new Routing\ConfigurationRoutesProvider($mockConfigurationManager, $mockObjectManager);
+        $this->assertEquals($expectedRoutes, $configurationRoutesProvider->getRoutes());
+    }
+
+    /**
+     * @test
+     */
+    public function configuredProvidersAreCalledToGenerateSubroutes(): void
+    {
+        $configuration = [
+            [
+                'name' => 'Routes provider without options',
+                'providerFactory' => 'Vendor\Example\RoutesProvider',
+            ],
+            [
+                'name' => 'Routes provider with options',
+                'providerFactory' => 'Vendor\Example\RoutesProviderWithOptions',
+                'providerOptions' => ['foo' => 'bar'],
+            ],
+        ];
+
+        $mockRoutesProvider = $this->createMock(Routing\RoutesProviderInterface::class);
+        $mockRoutesProviderWithOptions = $this->createMock(Routing\RoutesProviderInterface::class);
+
+        $mockRoutesProviderFactory = $this->createMock(Routing\RoutesProviderFactoryInterface::class);
+        $mockRoutesProviderFactory->expects($this->once())
+            ->method('createRoutesProvider')
+            ->with([])
+            ->willReturn($mockRoutesProvider);
+
+        $mockRoutesProviderWithOptionsFactory = $this->createMock(Routing\RoutesProviderFactoryInterface::class);
+        $mockRoutesProviderWithOptionsFactory->expects($this->once())
+            ->method('createRoutesProvider')
+            ->with(['foo' => 'bar'])
+            ->willReturn($mockRoutesProviderWithOptions);
+
+        $mockObjectManager = $this->createMock(ObjectManagerInterface::class);
+        $mockObjectManager->expects($this->exactly(2))->method('get')->willReturnCallback(
+            function (string $name) use ($mockRoutesProviderFactory, $mockRoutesProviderWithOptionsFactory) {
+                return match ($name) {
+                    'Vendor\Example\RoutesProvider' => $mockRoutesProviderFactory,
+                    'Vendor\Example\RoutesProviderWithOptions' => $mockRoutesProviderWithOptionsFactory
+                };
+            }
+        );
+
+        $mockConfigurationManager = $this->createMock(ConfigurationManager::class);
+        $mockConfigurationManager->expects($this->once())->method('getConfiguration')->with(ConfigurationManager::CONFIGURATION_TYPE_ROUTES)->willReturn($configuration);
+
+        $expectedRoute1 = new Route();
+        $expectedRoute1->setName('Route 1');
+        $expectedRoute1->setUriPattern('route1/{@package}/{@controller}/{@action}(.{@format})');
+        $expectedRoute1->setDefaults(['@format' => 'html']);
+
+        $expectedRoute2 = new Route();
+        $expectedRoute2->setName('Route 2');
+        $expectedRoute2->setUriPattern('route2/{@package}/{@controller}/{@action}(.{@format})');
+
+        $mockRoutesProvider->expects($this->once())->method('getRoutes')->willReturn(Routes::create($expectedRoute1));
+        $mockRoutesProviderWithOptions->expects($this->once())->method('getRoutes')->willReturn(Routes::create($expectedRoute2));
+
+        $expectedRoutes = Routes::create($expectedRoute1, $expectedRoute2);
+
+        $configurationRoutesProvider = new Routing\ConfigurationRoutesProvider($mockConfigurationManager, $mockObjectManager);
         $this->assertEquals($expectedRoutes, $configurationRoutesProvider->getRoutes());
     }
 }
diff --git a/Neos.Flow/Tests/Unit/Mvc/Routing/Dto/RouteAnnotationTest.php b/Neos.Flow/Tests/Unit/Mvc/Routing/Dto/RouteAnnotationTest.php
new file mode 100644
index 0000000000..5069ab6e20
--- /dev/null
+++ b/Neos.Flow/Tests/Unit/Mvc/Routing/Dto/RouteAnnotationTest.php
@@ -0,0 +1,101 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Neos\Flow\Tests\Unit\Mvc\Routing\Dto;
+
+use Neos\Flow\Annotations as Flow;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Tests for #[Flow\Route]
+ */
+class RouteAnnotationTest extends TestCase
+{
+    /**
+     * @test
+     */
+    public function simpleRoutes()
+    {
+        $route = new Flow\Route(uriPattern: 'my/path');
+        self::assertSame('my/path', $route->uriPattern);
+        self::assertSame('', $route->name);
+        self::assertSame([], $route->httpMethods);
+        self::assertSame([], $route->defaults);
+
+        $route = new Flow\Route(
+            uriPattern: 'my/other/path',
+            name: 'specialRoute',
+            httpMethods: ['POST'],
+            defaults: ['test' => 'foo']
+        );
+        self::assertSame('my/other/path', $route->uriPattern);
+        self::assertSame('specialRoute', $route->name);
+        self::assertSame(['POST'], $route->httpMethods);
+        self::assertSame(['test' => 'foo'], $route->defaults);
+    }
+
+    /**
+     * @test
+     */
+    public function preservedDefaults()
+    {
+        $this->expectExceptionCode(1711129638);
+
+        new Flow\Route(uriPattern: 'my/path', defaults: ['@action' => 'index']);
+    }
+
+    /**
+     * @test
+     */
+    public function preservedInUriPattern()
+    {
+        $this->expectExceptionCode(1711129634);
+
+        new Flow\Route(uriPattern: 'my/{@package}');
+    }
+
+    /**
+     * @test
+     */
+    public function uriPatternMustNotStartWithLeadingSlash()
+    {
+        $this->expectExceptionCode(1711529592);
+
+        new Flow\Route(uriPattern: '/absolute');
+    }
+
+    /**
+     * @test
+     */
+    public function uriPatternMustNotBeEmpty()
+    {
+        $this->expectExceptionCode(1711529592);
+
+        new Flow\Route(uriPattern: '');
+    }
+
+    /**
+     * @test
+     */
+    public function httpMethodMustNotBeEmptyString()
+    {
+        $this->expectExceptionCode(1711530485);
+
+        new Flow\Route(uriPattern: 'foo', httpMethods: ['']);
+    }
+
+    /**
+     * @test
+     */
+    public function httpMethodMustBeUpperCase()
+    {
+        $this->expectExceptionCode(1711530485);
+
+        /** @see \Neos\Flow\Mvc\Routing\Route::matches() where we do case-sensitive comparison against uppercase */
+        new Flow\Route(
+            uriPattern: 'my/other/path',
+            httpMethods: ['post']
+        );
+    }
+}