diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index c7a6c898a7f1f79f1dfb695f00b46bc4c8ac1834..a7048bdd3f750a0b6cebb6f7e99c2b74e708c3db 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -127,6 +127,7 @@ class RouteMap
         }
 
         $this->addAuthenticatedAvatarRoutes($group);
+        $this->addAuthenticatedMvvRoutes($group);
         $this->addAuthenticatedEventsRoutes($group);
         $this->addAuthenticatedFeedbackRoutes($group);
         $this->addAuthenticatedFilesRoutes($group);
@@ -393,6 +394,8 @@ class RouteMap
         $group->get('/sem-classes/{id}/sem-types', Routes\Courses\SemTypesBySemClassIndex::class);
         $group->get('/sem-types', Routes\Courses\SemTypesIndex::class);
         $group->get('/sem-types/{id}', Routes\Courses\SemTypesShow::class);
+
+        $group->get('/module-components/{id}/courses', Routes\Courses\CoursesByModuleComponentsIndex::class);
     }
 
     private function addAuthenticatedCoursewareRoutes(RouteCollectorProxy $group): void
@@ -697,6 +700,28 @@ class RouteMap
         $group->get('/user-filter-fields/{id}', Routes\UserFilters\UserFilterFieldsShow::class);
     }
 
+    private function addAuthenticatedMvvRoutes(RouteCollectorProxy $group): void
+    {
+        $group->get('/courses-of-study', Routes\Mvv\CoursesOfStudyIndex::class);
+        $group->get('/courses-of-study/{id}', Routes\Mvv\CoursesOfStudyShow::class);
+        $group->get('/courses-of-study/{id}/components', Routes\Mvv\ComponentsByCoursesOfStudyIndex::class);
+        $group->get('/course-of-study-components/{id}/versions', Routes\Mvv\VersionsByCourseOfStudyComponentsIndex::class);
+        $group->get('/course-of-study-components/{id}/subject', Routes\Mvv\SubjectsByCourseOfStudyComponentsShow::class);
+        $group->get('/courses-of-study/{id}/degree', Routes\Mvv\DegreesByCoursesOfStudyShow::class);
+        $group->get('/degrees', Routes\Mvv\DegreesIndex::class);
+        $group->get('/degrees/{id}', Routes\Mvv\DegreesShow::class);
+        $group->get('/subjects',Routes\Mvv\SubjectsIndex::class);
+        $group->get('/subjects/{id}',Routes\Mvv\SubjectsShow::class);
+        $group->get('/component-versions/{id}', Routes\Mvv\ComponentVersionsShow::class);
+        $group->get('/modules', Routes\Mvv\ModulesIndex::class);
+        $group->get('/modules/{id}', Routes\Mvv\ModulesShow::class);
+        $group->get('/modules/{id}/module-components', Routes\Mvv\ModuleComponentsByModuleIndex::class);
+        $group->get('/module-components/{id}', Routes\Mvv\ModuleComponentsShow::class);
+        // not a JSON:API route
+        $group->get('/component-version-deep/{id}', Routes\Mvv\ComponentVersionsDeep::class);
+
+    }
+
     private function addRelationship(RouteCollectorProxy $group, string $url, string $handler): void
     {
         $group->map(['GET', 'PATCH', 'POST', 'DELETE'], $url, $handler);
diff --git a/lib/classes/JsonApi/Routes/Courses/CoursesByModuleComponentsIndex.php b/lib/classes/JsonApi/Routes/Courses/CoursesByModuleComponentsIndex.php
new file mode 100644
index 0000000000000000000000000000000000000000..e7eaee9e610331015e7aa1abdba65c859215639c
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courses/CoursesByModuleComponentsIndex.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace JsonApi\Routes\Courses;
+
+use Modulteil;
+use Course;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Semester;
+
+class CoursesByModuleComponentsIndex extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        'blubber-threads',
+        'end-semester',
+        'events',
+        'feedback-elements',
+        'file-refs',
+        'folders',
+        'forum-categories',
+        'institute',
+        'memberships',
+        'news',
+        'participating-institutes',
+        'sem-class',
+        'sem-type',
+        'start-semester',
+        'status-groups',
+        'wiki-pages',
+    ];
+
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    protected $allowedFilteringParameters = ['semester', 'df'];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, ?array $args): Response
+    {
+        $component = Modulteil::find($args['id']);
+        if (!$component) {
+            throw new RecordNotFoundException();
+        }
+
+        $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+
+        $error = $this->validateFilters($filtering);
+        if ($error) {
+            throw new BadRequestException($error);
+        }
+
+        $courses = $this->findCoursesByComponent(
+            $component,
+            $filtering
+        );
+        [$offset, $limit] = $this->getOffsetAndLimit();
+
+        return $this->getPaginatedContentResponse(
+            array_slice($courses, $offset, $limit),
+            count($courses)
+        );
+    }
+
+    private function validateFilters(array $filtering): ?string
+    {
+        // semester
+        if (
+            isset($filtering['semester'])
+            && !Semester::exists($filtering['semester'])
+        ) {
+            return 'Invalid "semester".';
+        }
+
+        // data fields
+        if (isset($filtering['df']) && is_array($filtering['df'])) {
+            $accepted_dfs = $this->getAcceptedDataFields();
+            foreach (array_keys($filtering['df']) as $df) {
+                if (!in_array($df, $accepted_dfs)) {
+                    return 'Invalid data field as filtering parameter.';
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get ids of accepted datafields for current user.
+     * Only simple types of bool, textline, selectbox and radio with global
+     * visibility for all users are accepted.
+     *
+     * @return array Accepted datafields
+     */
+    private function getAcceptedDataFields(): array
+    {
+        $data_fields = \DataField::findAndMapBySQL(
+            fn(\DataField $data_field) => $data_field->id,
+            "`object_type` = 'sem' AND `view_perms` = 'user'
+                AND `type` IN('bool', 'textline', 'selectbox', 'radio')"
+        );
+        return $data_fields;
+    }
+
+    private function getSemesterFilter(array $filtering): ?Semester
+    {
+        if (!isset($filtering['semester'])) {
+            return null;
+        }
+        return Semester::find($filtering['semester']);
+    }
+
+
+    /**
+     * Finds visible courses by given module component.
+     *
+     * @param Modulteil $component
+     * @param Semester|null $semester
+     *
+     * @return Course[] Visible courses assigned to module component
+     */
+    private function findCoursesByComponent(Modulteil $component, array $filtering): array
+    {
+        $course_ids = [];
+        foreach ($component->lvgruppen as $lvgruppe) {
+            $course_ids += $lvgruppe->courses->findBy('visible', '1')->pluck('id');
+        }
+        if (count($course_ids) === 0) {
+            return [];
+        }
+
+        if (isset($filtering['df']) && is_array($filtering['df'])) {
+            $df_course_ids = $course_ids;
+            foreach ($filtering['df'] as $id => $value) {
+                $df_course_ids = array_intersect($df_course_ids, \DatafieldEntryModel::findAndMapBySQL(
+                    fn($df) => $df->range_id,
+                    '`datafield_id` = ? AND `range_id` IN (?) AND `content` = ?',
+                    [$id, $course_ids, $value]
+                ));
+            }
+
+            $course_ids = array_merge_recursive($df_course_ids);
+        }
+        $courses = Course::findMany(
+            $course_ids,
+            'ORDER BY name'
+        );
+
+        $semester = $this->getSemesterFilter($filtering);
+        if ($semester) {
+            $courses = array_filter($courses, fn(\Course $course) => $course->isInSemester($semester));
+        }
+
+        return $courses;
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courses/CoursesIndex.php b/lib/classes/JsonApi/Routes/Courses/CoursesIndex.php
index d97cdc0b3c3a4314592126d800a6ba4d783b7b64..5c3e46ee6acfbc6334f0a06a247a2fb14cb2b758 100644
--- a/lib/classes/JsonApi/Routes/Courses/CoursesIndex.php
+++ b/lib/classes/JsonApi/Routes/Courses/CoursesIndex.php
@@ -10,7 +10,7 @@ use JsonApi\JsonApiController;
 
 class CoursesIndex extends JsonApiController
 {
-    protected $allowedFilteringParameters = ['q', 'fields', 'semester', 'category', 'scope_choose', 'range_choose'];
+    protected $allowedFilteringParameters = ['q', 'fields', 'semester', 'category', 'scope_choose', 'range_choose', 'df'];
 
     protected $allowedIncludePaths = [
         'blubber-threads',
@@ -51,7 +51,7 @@ class CoursesIndex extends JsonApiController
         list($offset, $limit) = $this->getOffsetAndLimit();
 
         return $this->getPaginatedContentResponse(
-            \Course::findMany(array_slice($courseIds, $offset, $limit)),
+            $this->getCourses(array_slice($courseIds, $offset, $limit)),
             count($courseIds)
         );
     }
@@ -80,6 +80,16 @@ class CoursesIndex extends JsonApiController
                 return 'Invalid "semester".';
             }
         }
+
+        // data fields
+        if (isset($filtering['df']) && is_array($filtering['df'])) {
+            $accepted_dfs = $this->getAcceptedDataFields();
+            foreach (array_keys($filtering['df']) as $df) {
+                if (!in_array($df, $accepted_dfs)) {
+                    return 'Invalid data field as filtering parameter.';
+                }
+            }
+        }
     }
 
     private function getContextFilters()
@@ -99,6 +109,47 @@ class CoursesIndex extends JsonApiController
         return array_merge($defaults, $filtering);
     }
 
+    private function getCourses(array $course_ids): array
+    {
+        $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+        if (isset($filtering['df']) && is_array($filtering['df'])) {
+            $df_where = [];
+            $params = [
+                $course_ids
+            ];
+            foreach ($filtering['df'] as $id => $value) {
+                $df_where[] = ' (`datafields_entries`.`datafield_id` = ? AND `datafields_entries`.`content` = ?) ';
+                $params[] = $id;
+                $params[] = $value;
+            }
+            return \Course::findBySQL("JOIN `datafields_entries`
+                ON `seminare`.`seminar_id` = `datafields_entries`.`range_id`
+                WHERE `seminare`.`seminar_id` IN (?)
+                AND " .
+                implode('AND', $df_where),
+                $params);
+        } else {
+            return \Course::findMany($course_ids);
+        }
+    }
+
+    /**
+     * Get ids of accepted datafields for current user.
+     * Only simple types of bool, textline, selectbox and radio with global
+     * visibility for all users are accepted.
+     *
+     * @return array
+     */
+    private function getAcceptedDataFields(): array
+    {
+        $data_fields = \DataField::findAndMapBySQL(
+            fn(\DataField $data_field) => $data_field->id,
+            "`object_type` = 'sem' AND `view_perms` = 'user'
+                AND `type` IN('bool', 'textline', 'selectbox', 'radio')"
+        );
+        return $data_fields;
+    }
+
     /**
      * @SuppressWarnings(PHPMD.Superglobals)
      */
diff --git a/lib/classes/JsonApi/Routes/Institutes/InstitutesIndex.php b/lib/classes/JsonApi/Routes/Institutes/InstitutesIndex.php
index 6eef1b63ccdceca3ffddb7ea7bc0ea381b84855d..d31cbe8866cb961a450f2dcaebf8d6c862172e60 100644
--- a/lib/classes/JsonApi/Routes/Institutes/InstitutesIndex.php
+++ b/lib/classes/JsonApi/Routes/Institutes/InstitutesIndex.php
@@ -13,6 +13,7 @@ class InstitutesIndex extends JsonApiController
         InstituteSchema::REL_FACULTY,
         InstituteSchema::REL_STATUS_GROUPS,
         InstituteSchema::REL_SUB_INSTITUTES,
+        InstituteSchema::REL_COURSES_OF_STUDY,
     ];
 
     protected $allowedFilteringParameters = ['is-faculty'];
diff --git a/lib/classes/JsonApi/Routes/Institutes/InstitutesShow.php b/lib/classes/JsonApi/Routes/Institutes/InstitutesShow.php
index 5ef3b5a8d02ea0c5d7d6bef03af05c526fea7a63..6a3e0a981848d1a5076fc422725bed99126e5b40 100644
--- a/lib/classes/JsonApi/Routes/Institutes/InstitutesShow.php
+++ b/lib/classes/JsonApi/Routes/Institutes/InstitutesShow.php
@@ -18,6 +18,7 @@ class InstitutesShow extends JsonApiController
         InstituteSchema::REL_FACULTY,
         InstituteSchema::REL_STATUS_GROUPS,
         InstituteSchema::REL_SUB_INSTITUTES,
+        InstituteSchema::REL_COURSES_OF_STUDY,
     ];
 
     /**
diff --git a/lib/classes/JsonApi/Routes/Mvv/Authority.php b/lib/classes/JsonApi/Routes/Mvv/Authority.php
new file mode 100644
index 0000000000000000000000000000000000000000..8c9dcf97669d76408e878689833a9868852746cd
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/Authority.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Studiengang;
+use Modul;
+use User;
+
+class Authority
+{
+    public static function canIndexCoursesOfStudy(User $user): bool
+    {
+        return true;
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.Superglobals)
+     */
+    public static function canShowCourseOfStudy(User $user, Studiengang $resource): bool
+    {
+        return $GLOBALS['perm']->have_perm('user') && self::isReadableStudyCourse($user, $resource);
+    }
+
+    private static function isReadableStudyCourse(User $user, Studiengang $resource)
+    {
+        $public_status = \ModuleManagementModel::getPublicStatus(Studiengang::class);
+        return in_array($resource->stat, $public_status)
+            || \RolePersistence::isAssignedRole($user->id, 'MVVAdmin');
+
+    }
+
+    public static function canIndexModules(User $user): bool
+    {
+        return true;
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public static function canShowModule(User $user, Modul $resource): bool
+    {
+        return $GLOBALS['perm']->have_perm('user') && self::isReadableModule($user, $resource);
+    }
+
+    private static function isReadableModule(User $user, Modul $resource): bool
+    {
+        $public_status = \ModuleManagementModel::getPublicStatus(Modul::class);
+        return in_array($resource->stat, $public_status)
+            || \RolePersistence::isAssignedRole($user->id, 'MVVAdmin');
+
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public static function canShowComponentVersion(User $user, \StgteilVersion $resource): bool
+    {
+        return $GLOBALS['perm']->have_perm('user') && self::isReadableComponentVersion($user, $resource);
+    }
+
+    private static function isReadableComponentVersion(User $user, \StgteilVersion $resource): bool
+    {
+        $public_status = \ModuleManagementModel::getPublicStatus(\StgteilVersion::class);
+        return in_array($resource->stat, $public_status)
+            || \RolePersistence::isAssignedRole($user->id, 'MVVAdmin');
+
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsDeep.php b/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsDeep.php
new file mode 100644
index 0000000000000000000000000000000000000000..7b81ccf8f986f68e5fc8bfde768deac579300013
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsDeep.php
@@ -0,0 +1,258 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Errors\BadRequestException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\NonJsonApiController;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\StreamFactoryInterface;
+
+class ComponentVersionsDeep extends NonJsonApiController
+{
+    protected $allowedFilteringParameters = ['q', 'institute', 'semester', 'section'];
+
+    public function __construct(
+        ContainerInterface $container,
+        private StreamFactoryInterface $streamFactory
+    ) {
+        parent::__construct($container);
+    }
+
+    public function __invoke(Request $request, Response $response, array $args)
+    {
+        $component_version = \StgteilVersion::find($args['id']);
+        if (!$component_version) {
+            throw new RecordNotFoundException();
+        }
+
+        $parameters = $request->getQueryParams();
+
+        $this->validateParameters($parameters);
+
+        $data = $this->getVersionData($component_version, $parameters);
+
+        return $response
+            ->withHeader('Content-Type', 'application/json')
+            ->withBody($this->streamFactory->createStream(json_encode($data)));
+    }
+
+    private function validateParameters(array $parameters): void
+    {
+        if (!isset($parameters['semester'])) {
+            throw new BadRequestException('Parameter semester is missing');
+        }
+
+        if (!\Semester::exists($parameters['semester'])) {
+            throw new BadRequestException('Semester not found');
+        }
+    }
+
+    private function getVersionData(\StgteilVersion $version, array $parameters): array
+    {
+        $data = [
+            'id' => $version->id,
+            'display-name' => $version->getDisplayName(),
+            'start-semester' => $version->start_semester ? $this->getSemesterData($version->start_semester) : '',
+            'end-semester' => $version->end_semester ? $this->getSemesterData($version->end_semester) : '',
+            'code' => $version->code,
+            'description' => $version->beschreibung,
+            'date-of-decision' => $version->beschlussdatum,
+            'edition-number' => $version->fassung_nr,
+            'edition-type' => \Config::get()->MVV_STGTEILVERSION['FASSUNG_TYP'][$version->fassung_typ] ?? '',
+            'status' => \Config::get()->MVV_STGTEILVERSION['STATUS']['values'][$version->stat],
+            'sections' => $this->getSectionsData($version, $parameters),
+        ];
+        return $data;
+    }
+
+    private function getSectionsData(\StgteilVersion $version, array $parameters): array
+    {
+        $data = [];
+        foreach ($version->abschnitte as $section) {
+            $data[] = [
+                'id' => $section->id,
+                'display-name' => $section->getDisplayName(),
+                'comment' => $section->kommentar,
+                'position' => $section->position,
+                'cp' => $section->kp,
+                'caption' => $section->ueberschrift,
+                'modules' => $this->getModulesData($section, $parameters),
+            ];
+        }
+        return $data;
+    }
+
+    private function getModulesData(\StgteilAbschnitt $section, array $parameters): array
+    {
+        $status = \Config::get()->MVV_MODUL['STATUS']['values'];
+        $semester = \Semester::find($parameters['semester']);
+        $modules_filtered = $section->module->filter(
+            fn(\Modul $module) =>
+                ((empty($module->start_semester) || $module->start_semester->beginn <= $semester->ende)
+                && (empty($module->end_semester) || $module->end_semester->ende >= $semester->beginn)
+                && $status[$module->stat]['public'] === 1)
+        );
+        $data = [];
+        foreach ($modules_filtered as $module) {
+            $data[] = [
+                'id' => $module->id,
+                'display-name' => (string) $module->getDisplayName(),
+                'code' => (string) $module->code,
+                'date-of-decision' => $module->beschlussdatum ? date('c', $module->beschlussdatum) : '',
+                'edition-number' => (string) $module->fassung_nr,
+                'edition-type' => \Config::get()->MVV_MODUL['FASSUNG_TYP'][$module->fassung_typ] ?? '',
+                'version-number' => (string) $module->version,
+                'semester-duration' => (string) $module->dauer,
+                'capacity' => (string) $module->kapazitaet,
+                'cp' => $module->kp,
+                'workload-self' => (string) $module->wl_selbst,
+                'workload-exam' => (string) $module->wl_pruef,
+                'examination-period' => \Config::get()->MVV_MODUL['PRUEF_EBENE']['values'][$module->pruef_ebene] ?? '',
+                'grade-factor' => (string) $module->faktor_note,
+                'foreign-key' => (string) $module->flexnow_modul,
+                'name' => (string) $module->deskriptoren->bezeichnung,
+                'responsible' => (string) $module->deskriptoren->verantwortlich,
+                'prerequisite' => (string) $module->deskriptoren->voraussetzung,
+                'objectives' => (string) $module->deskriptoren->kompetenzziele,
+                'content' => (string) $module->deskriptoren->inhalte,
+                'literature' => (string) $module->deskriptoren->literatur,
+                'links' => (string) $module->deskriptoren->links,
+                'comment' => (string) $module->deskriptoren->kommentar,
+                'cycle' => (string) $module->deskriptoren->turnus,
+                'comment-capacity' => (string) $module->deskriptoren->kommentar_kapazitaet,
+                'comment_sws' => (string) $module->deskriptoren->kommentar_sws,
+                'status' => \Config::get()->MVV_MODUL['STATUS']['values'][$module->stat],
+                'module-languages' => $this->getModuleLanguagesData($module),
+                'module-section-data' => $this->getModuleSectionData(
+                    $module->abschnitte_modul->findOneBy('abschnitt_id', $section->id)),
+                'module-components' => $this->getModuleComponentsData($module, $parameters),
+                'start-semester' => $module->start_semester ? $this->getSemesterData($module->start_semester) : '',
+                'end-semester' => $module->end_semester ? $this->getSemesterData($module->end_semester) : '',
+            ];
+        }
+        return $data;
+    }
+
+    private function getSemesterData(\Semester $semester): array
+    {
+        return [
+            'id' => $semester->id,
+            'name' => $semester->name,
+            'short-name' => $semester->semester_token,
+            'semester-start' => $semester->beginn,
+            'semester-end' => $semester->ende,
+            'foreign-key' => $semester->external_id,
+            'teaching-start' => $semester->vorles_beginn,
+            'teaching-end' => $semester->vorles_ende,
+            'semester-switch-time' => $semester->sem_wechsel,
+        ];
+    }
+
+    private function getModuleComponentsData(\Modul $module, array $parameters): array
+    {
+        foreach ($module->modulteile as $component) {
+            $data[] = [
+                'id' => $component->id,
+                'name' => (string) $component->deskriptoren->bezeichnung,
+                'position' => $component->position,
+                'foreign-key' => $component->flexnow_modul,
+                'number' => $component->nummer,
+                'number-label' => \Config::get()->MVV_MODULTEIL['NUM_BEZEICHNUNG']['values'][$component->num_bezeichnung] ?? '',
+                'teaching-method' => \Config::get()->MVV_MODULTEIL['LERNLEHRFORM']['values'][$component->lernlehrform] ?? '',
+                'semester' => $component->semester,
+                'number-of-participants' => $component->kapazitaet,
+                'cp' => $component->kp,
+                'sws' => $component->sws,
+                'workload-compulsory' => $component->wl_praesenz,
+                'workload-preparation' => $component->wl_bereitung,
+                'workload-self' => $component->wl_selbst,
+                'workload-exam' => $component->wl_pruef,
+                'share-of-grade' => $component->anteil_note,
+                'compensable' => $component->ausgleichbar,
+                'compulsory-attendance' => $component->pflicht,
+                'prerequisites' => $component->deskriptoren->voraussetzung,
+                'comment' => $component->deskriptoren->kommentar,
+                'comment-capacity' => $component->deskriptoren->kommentar_kapazitaet,
+                'comment-wl-compulsory' => $component->deskriptoren->kommentar_wl_praesenz,
+                'comment-wl-preparation' => $component->deskriptoren->kommentar_wl_bereitung,
+                'comment-wl-self' => $component->deskriptoren->kommentar_wl_selbst,
+                'comment-wl-exam' => $component->deskriptoren->kommentar_wl_pruef,
+                'exam-prerequisites' => $component->deskriptoren->pruef_vorleistung,
+                'exam-requirements' => $component->deskriptoren->pruef_leistung,
+                'comment-compulsory-attendance' => $component->deskriptoren->kommentar_pflicht,
+                'courses' => $this->getCoursesData($component, $parameters),
+                'course-semesters' => $this->getModuleComponentSectionData($component),
+            ];
+        }
+        return $data;
+    }
+
+    private function getCoursesData(\Modulteil $component, array $parameters): array
+    {
+        $course_ids = [];
+        foreach ($component->lvgruppen as $lvgruppe) {
+            $course_ids += $lvgruppe->courses->pluck('id');
+        }
+
+        if (count($course_ids) === 0) {
+            return [];
+        }
+
+        $courses = \Course::findBySQL(
+            '`seminar_id` IN (?) AND `visible` = 1 ORDER BY start_time, name',
+            [$course_ids]
+        );
+        $semester = \Semester::find($parameters['semester']);
+        $courses = array_filter($courses, fn (\Course $course) => $course->isInSemester($semester));
+
+        $data = [];
+        foreach ($courses as $course) {
+            $data[] = [
+                'id' => $course->id,
+                'name' => $course->name,
+                'number' => $course->veranstaltungsnummer
+            ];
+        }
+        return $data;
+    }
+
+    private function getModuleComponentSectionData(\Modulteil $component): array
+    {
+        $data = [];
+        foreach ($component->abschnitt_assignments as $assignment) {
+            $data = [
+                'course-semester' => $assignment->fachsemester,
+                'differentiation' => $assignment->differenzierung,
+            ];
+        }
+        return $data;
+    }
+
+    private function getModuleSectionData(\StgteilabschnittModul $module_section): array
+    {
+        return [
+            'id' => $module_section->abschnitt_modul_id,
+            'name' => $module_section->bezeichnung,
+            'code' => $module_section->modulcode,
+            'position' => $module_section->position,
+            'foreign-key' => $module_section->flexnow_modul,
+        ];
+    }
+
+    private function getModuleLanguagesData(\Modul $module): array
+    {
+        $languages = \Config::get()->MVV_MODUL['SPRACHE']['values'];
+        $data = [];
+        foreach ($module->languages as $language) {
+            $data[] = [
+                'language' => $language->lang,
+                'name' => $languages[$language->lang]['name'],
+                'position' => $language->position,
+            ];
+        }
+        return $data;
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsShow.php b/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsShow.php
new file mode 100644
index 0000000000000000000000000000000000000000..248cc696c5529883033360b7d3dbe7b1004b33b7
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ComponentVersionsShow.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class ComponentVersionsShow extends JsonApiController
+{
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $version = \StgteilVersion::find($args['id']);
+        if (!$version) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowComponentVersion($this->getUser($request), $version)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($version);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ComponentsByCoursesOfStudyIndex.php b/lib/classes/JsonApi/Routes/Mvv/ComponentsByCoursesOfStudyIndex.php
new file mode 100644
index 0000000000000000000000000000000000000000..17fbdcfc0c53f04398f439ba0a69800db50fca2c
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ComponentsByCoursesOfStudyIndex.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\CourseOfStudyComponent;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class ComponentsByCoursesOfStudyIndex extends JsonApiController
+{
+    protected $allowedPagingParameters = [
+        'offset',
+        'limit'
+    ];
+
+    protected $allowedIncludePaths = [
+        CourseOfStudyComponent::REL_SUBJECT,
+        CourseOfStudyComponent::REL_VERSIONS,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $course_of_study = \Studiengang::find($args['id']);
+        if (!$course_of_study) {
+            throw new RecordNotFoundException();
+        }
+        [$offset, $limit] = $this->getOffsetAndLimit();
+
+        return $this->getPaginatedContentResponse(
+            $course_of_study->studiengangteile->limit($offset, $limit),
+            count($course_of_study->studiengangteile)
+        );
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/CourseOfStudyComponentsShow.php b/lib/classes/JsonApi/Routes/Mvv/CourseOfStudyComponentsShow.php
new file mode 100644
index 0000000000000000000000000000000000000000..496959177b7ba77e913f36e21e045e421af2636d
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/CourseOfStudyComponentsShow.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\CourseOfStudyComponent;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class CourseOfStudyComponentsShow extends JsonApiController
+{
+
+    protected $allowedIncludePaths = [
+        CourseOfStudyComponent::REL_SUBJECT,
+        CourseOfStudyComponent::REL_VERSIONS,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $component = \StudiengangTeil::find($args['id']);
+        if (!$component) {
+            throw new RecordNotFoundException();
+        }
+
+        return $this->getContentResponse($component);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyIndex.php b/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyIndex.php
new file mode 100644
index 0000000000000000000000000000000000000000..2040153ec01a85d6c600d3ce0e1f01611bc1af9b
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyIndex.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\CourseOfStudy;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\JsonApiController;
+
+class CoursesOfStudyIndex extends JsonApiController
+{
+    protected $allowedFilteringParameters = ['q', 'institute', 'semester', 'degree', 'category', 'type'];
+
+    protected $allowedIncludePaths = [
+        CourseOfStudy::REL_SECTIONS,
+        CourseOfStudy::REL_INSTITUTE,
+        CourseOfStudy::REL_COMPONENTS,
+        CourseOfStudy::REL_DEGREE,
+        CourseOfStudy::REL_END_SEMESTER,
+        CourseOfStudy::REL_START_SEMESTER,
+    ];
+
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!Authority::canIndexCoursesOfStudy($user = $this->getUser($request))) {
+            throw new AuthorizationFailedException();
+        }
+
+        $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+        $error = $this->validateFilters($filtering);
+        if ($error) {
+            throw new BadRequestException($error);
+        }
+
+        [$offset, $limit] = $this->getOffsetAndLimit();
+        $courses_of_study = $this->getCoursesOfStudy($filtering, $offset, $limit);
+
+        return $this->getPaginatedContentResponse(
+            $courses_of_study,
+            count($courses_of_study)
+        );
+    }
+
+    private function validateFilters($filtering)
+    {
+        // keyword aka q
+        if (isset($filtering['q']) && mb_strlen($filtering['q']) < 3) {
+            return 'Search term too short.';
+        }
+
+        // institute
+        if (isset($filtering['institute']) && !\Institute::exists($filtering['institute'])) {
+            return 'Filter `institute` must be a valid id.';
+        }
+
+        // degree
+        if (isset($filtering['degree']) && !\Abschluss::exists($filtering['degree'])) {
+            return 'Filter `degree` must be a valid id.';
+        }
+
+        // degree category
+        if (isset($filtering['category']) && !\AbschlussKategorie::find($filtering['category'])) {
+            return 'Filter `category` must be a valid id';
+        }
+
+        // semester
+        if (isset($filtering['semester']) && !\Semester::exists($filtering['semester'])) {
+            return 'Filter `semester` must be a valid id.';
+        }
+    }
+
+    private function getCoursesOfStudy($filtering, $offset, $limit): array
+    {
+        $join = '';
+        $where = ' 1 ';
+        $filtering['offset'] = $offset;
+        $filtering['limit'] = $limit;
+        if (isset($filtering['institute'])) {
+            $where .= ' AND `institut_id` = :institute ';
+        }
+        if (isset($filtering['type'])) {
+            $where .= ' AND `typ` = :type ';
+        }
+        if (isset($filtering['degree'])) {
+            $where .= ' AND `mvv_studiengang`.`abschluss_id` = :degree ';
+        }
+        if (isset($filtering['category'])) {
+            $join .= 'LEFT JOIN `mvv_abschluss_zuord` USING(`abschluss_id`) ';
+            $where .= ' AND `mvv_abschluss_zuord`.`kategorie_id` = :category';
+        }
+        if (isset($filtering['semester'])) {
+            $semester = \Semester::find($filtering['semester']);
+            unset($filtering['semester']);
+            $filtering['semester_start'] = $semester->beginn;
+            $filtering['semester_end'] = $semester->ende;
+            $join .= 'LEFT JOIN `semester_data` AS `start_sem`
+                        ON (`mvv_studiengang`.`start` = `start_sem`.`semester_id`)
+                    LEFT JOIN `semester_data` AS `end_sem`
+                        ON (`mvv_studiengang`.`end` = `end_sem`.`semester_id`) ';
+            $where .= ' AND (`start_sem`.`beginn` <= :semester_end OR ISNULL(`start_sem`.`beginn`))
+                        AND (`end_sem`.`ende` >= :semester_start OR ISNULL(`end_sem`.`ende`))';
+        }
+        if (isset($filtering['q'])) {
+            $where .= " AND (`mvv_studiengang`.`name` LIKE CONCAT('%', :q, '%') OR `mvv_studiengang`.`name_kurz` LIKE CONCAT('%', :q, '%')) ";
+        }
+        $where .= ' ORDER BY `mvv_studiengang`.`name` ASC
+                    LIMIT :limit OFFSET :offset';
+        return \Studiengang::findBySQL(
+            ($join ? $join . ' WHERE ' : '') . $where,
+            $filtering
+        );
+    }
+
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyShow.php b/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyShow.php
new file mode 100644
index 0000000000000000000000000000000000000000..8e8504a2dfd50d06582e588ef616d6e048b3317b
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/CoursesOfStudyShow.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class CoursesOfStudyShow extends JsonApiController
+{
+
+    protected $allowedIncludePaths = null;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $course_of_study = \Studiengang::find($args['id']);
+        if (!$course_of_study) {
+            throw new RecordNotFoundException();
+        }
+        if (!Authority::canShowCourseOfStudy($user = $this->getUser($request), $course_of_study)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($course_of_study);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/DegreesByCoursesOfStudyShow.php b/lib/classes/JsonApi/Routes/Mvv/DegreesByCoursesOfStudyShow.php
new file mode 100644
index 0000000000000000000000000000000000000000..006b9e7b200b1374c935fe5ae2241fcff875bd3d
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/DegreesByCoursesOfStudyShow.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class DegreesByCoursesOfStudyShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $course_of_study = \Studiengang::find($args['id']);
+        if (empty($course_of_study->abschluss)) {
+            throw new RecordNotFoundException('Could not find degree.');
+        }
+
+        return $this->getContentResponse($course_of_study->abschluss);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/DegreesIndex.php b/lib/classes/JsonApi/Routes/Mvv/DegreesIndex.php
new file mode 100644
index 0000000000000000000000000000000000000000..bff4c14d16957bbb5fbcf551f0475926cd638a3c
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/DegreesIndex.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Abschluss;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\JsonApiController;
+
+class DegreesIndex extends JsonApiController
+{
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        [$offset, $limit] = $this->getOffsetAndLimit();
+
+        return $this->getPaginatedContentResponse(
+            Abschluss::findBySQL("1 ORDER BY name LIMIT {$offset}, {$limit}"),
+            Abschluss::countBySql('1')
+        );
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/DegreesShow.php b/lib/classes/JsonApi/Routes/Mvv/DegreesShow.php
new file mode 100644
index 0000000000000000000000000000000000000000..ce521e8d995406f1d41c6459cf89f12f4f4c1fec
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/DegreesShow.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class DegreesShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $degree = \Abschluss::find($args['id']);
+        if (!$degree) {
+            throw new RecordNotFoundException('Could not find degree.');
+        }
+
+        return $this->getContentResponse($degree);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsByModuleIndex.php b/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsByModuleIndex.php
new file mode 100644
index 0000000000000000000000000000000000000000..9fb2b0bc32ad5e042e2695e29e996c5fe9e71894
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsByModuleIndex.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\ModuleComponent;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class ModuleComponentsByModuleIndex extends JsonApiController
+{
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    protected $allowedIncludePaths = [
+        ModuleComponent::REL_COURSES,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $module = \Modul::find($args['id']);
+        if (!$module) {
+            throw new RecordNotFoundException();
+        }
+        [$offset, $limit] = $this->getOffsetAndLimit();
+
+        return $this->getPaginatedContentResponse(
+            $module->modulteile->limit($offset, $limit),
+            count($module->modulteile)
+        );
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsShow.php b/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsShow.php
new file mode 100644
index 0000000000000000000000000000000000000000..a3c4d440bb7e4538de981933df0702edf20666bf
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ModuleComponentsShow.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\ModuleComponent;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class ModuleComponentsShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        ModuleComponent::REL_COURSES,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $component = \Modulteil::find($args['id']);
+        if (!$component) {
+            throw new RecordNotFoundException('Could not find module component.');
+        }
+
+        return $this->getContentResponse($component);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ModulesIndex.php b/lib/classes/JsonApi/Routes/Mvv/ModulesIndex.php
new file mode 100644
index 0000000000000000000000000000000000000000..3018fd404e1c14f9f4de0efad605e352d526abfc
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ModulesIndex.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\Schemas\Module;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\JsonApiController;
+
+class ModulesIndex extends JsonApiController
+{
+    protected $allowedFilteringParameters = ['q', 'institute', 'semester', 'section'];
+
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    protected $allowedIncludePaths = [
+        Module::REL_MODULE_COMPONENTS,
+        Module::REL_END_SEMESTER,
+        Module::REL_START_SEMESTER,
+        Module::REL_RESPONSIBLE_DEPARTMENT,
+        Module::REL_DEPARTMENTS,
+        Module::REL_SOURCE_MODULE,
+        Module::REL_VARIANT_MODULE,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        if (!Authority::canIndexModules($user = $this->getUser($request))) {
+            throw new AuthorizationFailedException();
+        }
+
+        $filtering = $this->getQueryParameters()->getFilteringParameters() ?: [];
+        $error = $this->validateFilters($filtering);
+        if ($error) {
+            throw new BadRequestException($error);
+        }
+
+        [$offset, $limit] = $this->getOffsetAndLimit();
+        $modules = $this->getModules($filtering, $offset, $limit);
+
+        return $this->getPaginatedContentResponse(
+            $modules,
+            count($modules)
+        );
+    }
+
+    private function validateFilters($filtering)
+    {
+        // keyword aka q
+        if (isset($filtering['q']) && mb_strlen($filtering['q']) < 3) {
+            return 'Search term too short.';
+        }
+
+        // institute
+        if (isset($filtering['institute']) && !\Institute::exists($filtering['institute'])) {
+            return 'Filter `institute` must be a valid id.';
+        }
+
+        // section
+        if (isset($filtering['section']) && !\StgteilAbschnitt::exists($filtering['section'])) {
+            return 'Filter `section` must be a valid id';
+        }
+
+        // semester
+        if (isset($filtering['semester']) && !\Semester::exists($filtering['semester'])) {
+            return 'Filter `semester` must be a valid id.';
+        }
+    }
+
+    private function getModules($filtering, $offset, $limit): array
+    {
+        $join = '';
+        $where = ' 1 ';
+        $filtering['offset'] = $offset;
+        $filtering['limit'] = $limit;
+        if (isset($filtering['institute'])) {
+            $where .= ' AND `institut_id` = :institute ';
+        }
+        if (isset($filtering['section'])) {
+            $join .= 'LEFT JOIN `mvv_stgteilabschnitt_modul` USING(`modul_id`) ';
+            $where .= ' AND `mvv_stgteilabschnitt_modul`.`abschnitt_id` = :section';
+        }
+        if (isset($filtering['semester'])) {
+            $semester = \Semester::find($filtering['semester']);
+            unset($filtering['semester']);
+            $filtering['semester_start'] = $semester->beginn;
+            $filtering['semester_end'] = $semester->ende;
+            $join .= 'LEFT JOIN `semester_data` AS `start_sem`
+                        ON (`mvv_modul`.`start` = `start_sem`.`semester_id`)
+                    LEFT JOIN `semester_data` AS `end_sem`
+                        ON (`mvv_modul`.`end` = `end_sem`.`semester_id`) ';
+            $where .= ' AND (`start_sem`.`beginn` <= :semester_end OR ISNULL(`start_sem`.`beginn`))
+                        AND (`end_sem`.`ende` >= :semester_start OR ISNULL(`end_sem`.`ende`))';
+        }
+        $join .= 'LEFT JOIN `mvv_modul_deskriptor` USING(`modul_id`) ';
+        if (isset($filtering['q'])) {
+            $where .= " AND (`mvv_modul_deskriptor`.`bezeichnung` LIKE CONCAT('%', :q, '%') OR `mvv_modul`.`code` LIKE CONCAT('%', :q, '%')) ";
+        }
+        $where .= ' ORDER BY `mvv_modul`.`code` ASC, `mvv_modul_deskriptor`.`bezeichnung` ASC
+                    LIMIT :limit OFFSET :offset';
+        return \Modul::findBySQL(
+            ($join ? $join . ' WHERE ' : '') . $where,
+            $filtering
+        );
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/ModulesShow.php b/lib/classes/JsonApi/Routes/Mvv/ModulesShow.php
new file mode 100644
index 0000000000000000000000000000000000000000..8db7773901044afc14ffb8af7074347b1f92c773
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/ModulesShow.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class ModulesShow extends JsonApiController
+{
+
+    protected $allowedIncludePaths = null;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $module = \Modul::find($args['id']);
+        if (!$module) {
+            throw new RecordNotFoundException();
+        }
+
+        if (!Authority::canShowModule($this->getUser($request), $module)) {
+            throw new AuthorizationFailedException();
+        }
+
+        return $this->getContentResponse($module);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/SubjectsByCourseOfStudyComponentsShow.php b/lib/classes/JsonApi/Routes/Mvv/SubjectsByCourseOfStudyComponentsShow.php
new file mode 100644
index 0000000000000000000000000000000000000000..cc19ee277ab5c1dcd72206682a97ca67d8656a4c
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/SubjectsByCourseOfStudyComponentsShow.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\Subject;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class SubjectsByCourseOfStudyComponentsShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        Subject::REL_DEPARTMENTS,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $component = \StudiengangTeil::find($args['id']);
+        if (empty($component->fach)) {
+            throw new RecordNotFoundException('Could not find subject.');
+        }
+
+        return $this->getContentResponse($component->fach);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/SubjectsIndex.php b/lib/classes/JsonApi/Routes/Mvv/SubjectsIndex.php
new file mode 100644
index 0000000000000000000000000000000000000000..338f71ab1012b224c5935135f1cd403ef40f13da
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/SubjectsIndex.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use Fach;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\JsonApiController;
+
+class SubjectsIndex extends JsonApiController
+{
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        [$offset, $limit] = $this->getOffsetAndLimit();
+
+        return $this->getPaginatedContentResponse(
+            Fach::findBySQL("1 ORDER BY name LIMIT {$offset}, {$limit}"),
+            Fach::countBySql('1')
+        );
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/SubjectsShow.php b/lib/classes/JsonApi/Routes/Mvv/SubjectsShow.php
new file mode 100644
index 0000000000000000000000000000000000000000..63428bc0c98b545478b240528992a25892078a5c
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/SubjectsShow.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\Subject;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class SubjectsShow extends JsonApiController
+{
+    protected $allowedIncludePaths = [
+        Subject::REL_DEPARTMENTS,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $subject = \Fach::find($args['id']);
+        if (!$subject) {
+            throw new RecordNotFoundException('Could not find subject.');
+        }
+
+        return $this->getContentResponse($subject);
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Mvv/VersionsByCourseOfStudyComponentsIndex.php b/lib/classes/JsonApi/Routes/Mvv/VersionsByCourseOfStudyComponentsIndex.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d321b9789e3677a286b3d8a80a3344d168f098d
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Mvv/VersionsByCourseOfStudyComponentsIndex.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace JsonApi\Routes\Mvv;
+
+use JsonApi\Schemas\ComponentVersion;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+use JsonApi\Errors\RecordNotFoundException;
+use JsonApi\JsonApiController;
+
+class VersionsByCourseOfStudyComponentsIndex extends JsonApiController
+{
+    protected $allowedPagingParameters = ['offset', 'limit'];
+
+    protected $allowedIncludePaths = [
+        ComponentVersion::REL_SECTIONS,
+        ComponentVersion::REL_START_SEMESTER,
+        ComponentVersion::REL_END_SEMESTER,
+    ];
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameters)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $component = \StudiengangTeil::find($args['id']);
+        if (!$component) {
+            throw new RecordNotFoundException();
+        }
+        [$offset, $limit] = $this->getOffsetAndLimit();
+
+        return $this->getPaginatedContentResponse(
+            $component->versionen->limit($offset, $limit),
+            count($component->versionen)
+        );
+    }
+}
diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php
index c651b8774dce590d4f422f08b34b3be7a04aee99..801bf293831782ec2d2ed33c04e9daec26968bfd 100644
--- a/lib/classes/JsonApi/SchemaMap.php
+++ b/lib/classes/JsonApi/SchemaMap.php
@@ -66,6 +66,15 @@ class SchemaMap
             \FolderType::class => Schemas\Folder::class,
             \UserFilter::class => Schemas\UserFilter::class,
             \UserFilterField::class => Schemas\UserFilterField::class,
+            \Studiengang::class => Schemas\CourseOfStudy::class,
+            \StudiengangTeil::class => Schemas\CourseOfStudyComponent::class,
+            \StgteilVersion::class => Schemas\ComponentVersion::class,
+            \Fach::class => Schemas\Subject::class,
+            \Abschluss::class => Schemas\Degree::class,
+            \Modul::class => Schemas\Module::class,
+            \Modulteil::class => Schemas\ModuleComponent::class,
+            \StgteilAbschnitt::class => Schemas\ComponentSection::class,
+
             \Courseware\Block::class => Schemas\Courseware\Block::class,
             \Courseware\BlockComment::class => Schemas\Courseware\BlockComment::class,
             \Courseware\BlockFeedback::class => Schemas\Courseware\BlockFeedback::class,
diff --git a/lib/classes/JsonApi/Schemas/ComponentSection.php b/lib/classes/JsonApi/Schemas/ComponentSection.php
new file mode 100644
index 0000000000000000000000000000000000000000..459d6e0791ab824087130e0f701b2a6081213a84
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/ComponentSection.php
@@ -0,0 +1,52 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class ComponentSection extends SchemaProvider
+{
+    const REL_MODULES = 'modules';
+    const TYPE = 'component-sections';
+
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'display-name' => (string) $resource->getDisplayName(),
+            'comment' => $resource->kommentar,
+            'position' => $resource->position,
+            'cp' => $resource->kp,
+            'caption' => $resource->ueberschrift,
+            'type' => get_class($resource)
+        ];
+    }
+
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        $relationships = $this->addModulesRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_MODULES));
+
+        return $relationships;
+    }
+
+    private function addModulesRelationship(array $relationships, $resource, $includeData)
+    {
+        $relationships[self::REL_MODULES] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_MODULES),
+            ],
+        ];
+
+        if ($includeData) {
+            $relationships[self::REL_MODULES][self::RELATIONSHIP_DATA] = $resource->module;
+        }
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/ComponentVersion.php b/lib/classes/JsonApi/Schemas/ComponentVersion.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea89716142d1d055eddd70503f5ad2436f3583bc
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/ComponentVersion.php
@@ -0,0 +1,94 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class ComponentVersion extends SchemaProvider
+{
+    const REL_SECTIONS = 'component-sections';
+    const REL_START_SEMESTER = 'start-semester';
+    const REL_END_SEMESTER = 'end-semester';
+    const TYPE = 'component-versions';
+
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'display-name' => (string) $resource->getDisplayName(),
+            'code' => (string) $resource->code,
+            'date' => (string) $resource->beschlussdatum,
+            'version-number' => (string) $resource->fassung_nr,
+            'version-type' => (string) $resource->fassung_typ,
+            'description' => (string) $resource->beschreibung,
+            'status' => (string) $resource->stat,
+            'status-name' => \Config::get()->MVV_STGTEILVERSION['STATUS']['values'][$resource->stat]['name'],
+            'type' => get_class($resource)
+        ];
+    }
+
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        if ($semester = $this->getStartSemester($resource)) {
+            $relationships[self::REL_START_SEMESTER] = $semester;
+        }
+        if ($semester = $this->getEndSemester($resource)) {
+            $relationships[self::REL_END_SEMESTER] = $semester;
+        }
+
+        $relationships = $this->addSectionsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SECTIONS));
+
+        return $relationships;
+    }
+
+    private function getStartSemester(\StgteilVersion $version)
+    {
+        $semester = \Semester::find($version->start_sem);
+        if (!$semester) {
+            return null;
+        }
+
+        return [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($semester),
+            ],
+            self::RELATIONSHIP_DATA => $semester,
+        ];
+    }
+
+    private function getEndSemester(\StgteilVersion $version)
+    {
+        $semester = \Semester::find($version->end_sem);
+        if (!$semester) {
+            return null;
+        }
+
+        return [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($semester),
+            ],
+            self::RELATIONSHIP_DATA => $semester,
+        ];
+    }
+
+    private function addSectionsRelationship(array $relationships, $resource, $includeData)
+    {
+        $relationships[self::REL_SECTIONS] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_SECTIONS),
+            ],
+        ];
+
+        if ($includeData) {
+            $relationships[self::REL_SECTIONS][self::RELATIONSHIP_DATA] = $resource->abschnitte;
+        }
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/CourseOfStudy.php b/lib/classes/JsonApi/Schemas/CourseOfStudy.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e6a0a34258bcc5c73457e93b1ecdd1a0920580e
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/CourseOfStudy.php
@@ -0,0 +1,151 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class CourseOfStudy extends SchemaProvider
+{
+    const REL_SECTIONS = 'sections';
+    const REL_INSTITUTE = 'institute';
+    const REL_COMPONENTS = 'components';
+    const REL_DEGREE = 'degree';
+    const REL_END_SEMESTER = 'end-semester';
+    const REL_START_SEMESTER = 'start-semester';
+    const TYPE = 'courses-of-study';
+
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'display-name' => (string) $resource->getDisplayName(),
+            'name' => (string) $resource->name,
+            'short-name' => (string) $resource->name_kurz,
+            'type' => (string) $resource->typ,
+            'status' => (string) $resource->stat,
+            'status-name' => \Config::get()->MVV_STUDIENGANG['STATUS']['values'][$resource->stat]['name'] ?? '',
+            'classname' => get_class($resource)
+        ];
+    }
+
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        $institute = \Institute::find($resource->institut_id);
+        if ($institute) {
+            $relationships[self::REL_INSTITUTE] = $this->getInstitute($resource, $this->shouldInclude($context, self::REL_INSTITUTE));
+        }
+
+        if ($semester = $this->getStartSemester($resource)) {
+            $relationships[self::REL_START_SEMESTER] = $semester;
+        }
+        if ($semester = $this->getEndSemester($resource)) {
+            $relationships[self::REL_END_SEMESTER] = $semester;
+        }
+
+        $relationships = $this->addSectionsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_SECTIONS));
+        $relationships = $this->addComponentsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_COMPONENTS));
+        $relationships = $this->addDegreeRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_DEGREE));
+
+        return $relationships;
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    private function getInstitute(\Studiengang $course_of_study, $shouldInclude)
+    {
+        $institute = \Institute::find($course_of_study->institut_id);
+        return $institute
+            ?  [
+                self::RELATIONSHIP_LINKS => [
+                    Link::RELATED => $this->createLinkToResource($institute),
+                ],
+                self::RELATIONSHIP_DATA => $institute,
+            ]
+            : [
+                self::RELATIONSHIP_DATA => null,
+            ];
+    }
+
+    private function getStartSemester(\Studiengang $course_of_study)
+    {
+        $semester = \Semester::find($course_of_study->start);
+        if (!$semester) {
+            return null;
+        }
+
+        return [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($semester),
+            ],
+            self::RELATIONSHIP_DATA => $semester,
+        ];
+    }
+
+    private function getEndSemester(\Studiengang $course_of_study)
+    {
+        $semester = \Semester::find($course_of_study->end);
+        if (!$semester) {
+            return null;
+        }
+
+        return [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($semester),
+            ],
+            self::RELATIONSHIP_DATA => $semester,
+        ];
+    }
+
+    private function addSectionsRelationship(array $relationships, $resource, $includeData)
+    {
+        $relationships[self::REL_SECTIONS] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_SECTIONS),
+            ],
+        ];
+
+        if ($includeData) {
+            $relationships[self::REL_SECTIONS][self::RELATIONSHIP_DATA] = $resource->stgteil_bezeichnungen;
+        }
+
+        return $relationships;
+    }
+
+    private function addComponentsRelationship(array $relationships, $resource, $includeData)
+    {
+        $relationships[self::REL_COMPONENTS] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_COMPONENTS),
+            ],
+        ];
+
+        if ($includeData) {
+            $relationships[self::REL_COMPONENTS][self::RELATIONSHIP_DATA] = $resource->studiengangteile;
+        }
+
+        return $relationships;
+    }
+
+    private function addDegreeRelationship(array $relationships, $resource, $includeData)
+    {
+        $relationships[self::REL_DEGREE] = [
+            self::RELATIONSHIP_LINKS_SELF => $this->createLinkToResource($resource->abschluss),
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_DEGREE),
+            ],
+        ];
+
+        if ($includeData) {
+            $relationships[self::REL_DEGREE][self::RELATIONSHIP_DATA] = $resource->abschluss;
+        }
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/CourseOfStudyComponent.php b/lib/classes/JsonApi/Schemas/CourseOfStudyComponent.php
new file mode 100644
index 0000000000000000000000000000000000000000..c351f121b20b4500630cbc21ba9ebf4d311a2ee8
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/CourseOfStudyComponent.php
@@ -0,0 +1,73 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class CourseOfStudyComponent extends SchemaProvider
+{
+    const REL_SUBJECT = 'subject';
+    const REL_VERSIONS = 'versions';
+    const TYPE = 'courses-of-study-components';
+
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'display-name' => (string) $resource->getDisplayName(),
+            'title-supplement' => (string) $resource->zusatz,
+            'cp' => (string) $resource->kp,
+            'semesters' => (string) $resource->semester,
+            'classname' => get_class($resource)
+        ];
+    }
+
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        if ($resource->fach) {
+            $relationships[self::REL_SUBJECT] = $this->getSubject($resource, $this->shouldInclude($context, self::REL_SUBJECT));
+        }
+
+        $relationships = $this->addVersionsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_VERSIONS));
+
+        return $relationships;
+    }
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    private function getSubject(\StudiengangTeil $component, $shouldInclude)
+    {
+        return $component->fach
+            ?  [
+                self::RELATIONSHIP_LINKS => [
+                    Link::RELATED => $this->createLinkToResource($component->fach),
+                ],
+                self::RELATIONSHIP_DATA => $component->fach,
+            ]
+            : [
+                self::RELATIONSHIP_DATA => null,
+            ];
+    }
+
+    private function addVersionsRelationship(array $relationships, $resource, $includeData)
+    {
+        $relationships[self::REL_VERSIONS] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_VERSIONS),
+            ],
+        ];
+
+        if ($includeData) {
+            $relationships[self::REL_VERSIONS][self::RELATIONSHIP_DATA] = $resource->versionen;
+        }
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/Institute.php b/lib/classes/JsonApi/Schemas/Institute.php
index 0084ca6baa0915f6798e340862a59693787932d1..61058f7457c5a449fd668aa23757ebaa38f0cc5c 100644
--- a/lib/classes/JsonApi/Schemas/Institute.php
+++ b/lib/classes/JsonApi/Schemas/Institute.php
@@ -16,6 +16,7 @@ class Institute extends SchemaProvider
     const REL_MEMBERSHIPS = 'memberships';
     const REL_STATUS_GROUPS = 'status-groups';
     const REL_SUB_INSTITUTES = 'sub-institutes';
+    const REL_COURSES_OF_STUDY = 'courses-of-study';
 
     /**
      * @param \Institute $institute
@@ -95,6 +96,12 @@ class Institute extends SchemaProvider
             $this->shouldInclude($context, self::REL_SUB_INSTITUTES)
         );
 
+        $relationships = $this->getCoursesOfStudyRelationship(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_COURSES_OF_STUDY)
+        );
+
         return $relationships;
     }
 
@@ -156,6 +163,30 @@ class Institute extends SchemaProvider
         return array_merge($relationships, [self::REL_STATUS_GROUPS => $relation]);
     }
 
+    private function getCoursesOfStudyRelationship(
+        array $relationships,
+        $resource,
+        $includeData
+    ): array {
+        $relation = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_COURSES_OF_STUDY),
+            ],
+        ];
+
+        if ($includeData) {
+            $relation[self::RELATIONSHIP_DATA] = $resource->courses_of_study;
+        } else {
+            $relation[self::RELATIONSHIP_DATA] = $resource->courses_of_study->map(function (\Studiengang $cos): \Studiengang {
+                return \Studiengang::build(['id' => $cos->id]);
+            });
+        }
+
+        $relationships[self::REL_COURSES_OF_STUDY] = $relation;
+
+        return $relationships;
+    }
+
     public function hasResourceMeta($resource): bool
     {
         return true;
@@ -168,6 +199,7 @@ class Institute extends SchemaProvider
     {
         return [
             'sub-institutes-count' => count($resource->sub_institutes),
+            'courses-of-study-count' => count($resource->courses_of_study),
         ];
     }
 }
diff --git a/lib/classes/JsonApi/Schemas/Module.php b/lib/classes/JsonApi/Schemas/Module.php
new file mode 100644
index 0000000000000000000000000000000000000000..55fe085992c96187d099c22bca9d3f37572968d8
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Module.php
@@ -0,0 +1,171 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class Module extends SchemaProvider
+{
+    const REL_DEPARTMENTS = 'departments';
+    const REL_RESPONSIBLE_DEPARTMENT = 'responsible-department';
+    const REL_SOURCE_MODULE = 'source-module';
+    const REL_VARIANT_MODULE = 'variant-module';
+    const REL_START_SEMESTER = 'start-semester';
+    const REL_END_SEMESTER = 'end-semester';
+    const REL_MODULE_COMPONENTS = 'module-components';
+    const REL_LANGUAGES = 'languages';
+
+    const TYPE = 'modules';
+
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'display-name' => (string) $resource->getDisplayName(),
+            'code' => (string) $resource->code,
+            'date-of-decision' => $resource->beschlussdatum ? date('c', $resource->beschlussdatum) : '',
+            'edition-number' => (string) $resource->fassung_nr,
+            'edition-type' => \Config::get()->MVV_STGTEILVERSION['FASSUNG_TYP'][$resource->fassung_typ] ?? '',
+            'version-number' => (string) $resource->version,
+            'semester-duration' => (string) $resource->dauer,
+            'capacity' => (string) $resource->kapazitaet,
+            'cp' => $resource->kp,
+            'workload-self' => (string) $resource->wl_selbst,
+            'workload-exam' => (string) $resource->wl_pruef,
+            'examination-period' => \Config::get()->MVV_MODUL['PRUEF_EBENE']['values'][$resource->pruef_ebene] ?? '',
+            'grade-factor' => (string) $resource->faktor_note,
+        //    'module-responsible' => (string) $resource->verantwortlich,
+            'foreign-key' => (string) $resource->flexnow_modul,
+            'name' => (string) $resource->deskriptoren->bezeichnung,
+            'responsible' => (string) $resource->deskriptoren->verantwortlich,
+            'prerequisite' => (string) $resource->deskriptoren->voraussetzung,
+            'objectives' => (string) $resource->deskriptoren->kompetenzziele,
+            'content' => (string) $resource->deskriptoren->inhalte,
+            'literature' => (string) $resource->deskriptoren->literatur,
+            'links' => (string) $resource->deskriptoren->links,
+            'comment' => (string) $resource->deskriptoren->kommentar,
+            'cycle' => (string) $resource->deskriptoren->turnus,
+            'comment-capacity' => (string) $resource->deskriptoren->kommentar_kapazitaet,
+            'comment_sws' => (string) $resource->deskriptoren->kommentar_sws,
+            'type' => get_class($resource),
+            'status' => \Config::get()->MVV_MODUL['STATUS']['values'][$resource->stat],
+        ];
+    }
+
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        if ($semester = $this->getStartSemester($resource)) {
+            $relationships[self::REL_START_SEMESTER] = $semester;
+        }
+        if ($semester = $this->getEndSemester($resource)) {
+            $relationships[self::REL_END_SEMESTER] = $semester;
+        }
+        if ($responsible_department = $this->getResponsibleDepartment($resource)) {
+            $relationships[self::REL_RESPONSIBLE_DEPARTMENT] = $responsible_department;
+        }
+        /*
+        if (!empty($resource->responsible_institute)) {
+            $relationships[self::REL_RESPONSIBLE_DEPARTMENT] =
+                 [
+                     self::RELATIONSHIP_LINKS => [
+                         Link::RELATED => $this->createLinkToResource($resource->responsible_institute->institute),
+                     ],
+                     self::RELATIONSHIP_DATA => $resource->responsible_institute,
+                 ];
+        }
+        */
+/*
+        $relationships = $this->addResponsibleDepartmentRelationship(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_RESPONSIBLE_DEPARTMENT)
+        );
+*/
+        $relationships = $this->addDepartments(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_DEPARTMENTS)
+        );
+        $relationships = $this->addModuleComponentsRelationship(
+            $relationships,
+            $resource,
+            $this->shouldInclude($context, self::REL_MODULE_COMPONENTS)
+        );
+
+        return $relationships;
+    }
+
+    private function getStartSemester(\Modul $modul)
+    {
+        if (!$semester = \Semester::find($modul->start)) {
+            return null;
+        }
+
+        return [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($semester),
+            ],
+            self::RELATIONSHIP_DATA => $semester,
+        ];
+    }
+
+    private function getEndSemester(\Modul $modul)
+    {
+        $semester = \Semester::find($modul->end);
+        if (!$semester) {
+            return null;
+        }
+
+        return [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($semester),
+            ],
+            self::RELATIONSHIP_DATA => $semester,
+        ];
+    }
+
+    private function getResponsibleDepartment(\Modul $modul)
+    {
+        $responsible_department = \Institute::build(['id' => $modul->responsible_institute->institut_id]);
+        if (!$responsible_department) {
+            return null;
+        }
+
+        return [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->createLinkToResource($responsible_department),
+            ],
+            self::RELATIONSHIP_DATA => $responsible_department,
+        ];
+    }
+
+    private function addDepartments(array $relationships, $resource, $includeData)
+    {
+        $departments = $resource->assigned_institutes->orderBy('position')->map(function (\ModulInst $module_inst) {
+            return \Institute::build(['id' => $module_inst->institut_id]);
+        });
+
+        $relationships[self::REL_DEPARTMENTS][self::RELATIONSHIP_DATA] = $departments;
+
+        return $relationships;
+    }
+
+    private function addModuleComponentsRelationship(array $relationships, $resource, $includeData)
+    {
+        $relationships[self::REL_MODULE_COMPONENTS] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_MODULE_COMPONENTS),
+            ],
+        ];
+
+        $relationships[self::REL_MODULE_COMPONENTS][self::RELATIONSHIP_DATA] = $resource->modulteile;
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/ModuleComponent.php b/lib/classes/JsonApi/Schemas/ModuleComponent.php
new file mode 100644
index 0000000000000000000000000000000000000000..7ff24e9046de8de9c700b72da4bb1d4a68dd6b30
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/ModuleComponent.php
@@ -0,0 +1,70 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class ModuleComponent extends SchemaProvider
+{
+    const REL_COURSES = 'courses';
+    const TYPE = 'module-components';
+
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'name' => (string) $resource->deskriptoren->bezeichnung,
+            'position' => $resource->position,
+            'foreign_key' => $resource->flexnow_modul,
+            'number' => $resource->nummer,
+            'number_label' => \Config::get()->MVV_MODULTEIL['NUM_BEZEICHNUNG']['values'][$resource->num_bezeichnung] ?? '',
+            'teaching_method' => \Config::get()->MVV_MODULTEIL['LERNLEHRFORM']['values'][$resource->lernlehrform] ?? '',
+            'semester' => $resource->semester,
+            'number_of_participants' => $resource->kapazitaet,
+            'cp' => $resource->kp,
+            'sws' => $resource->sws,
+            'workload_compulsory' => $resource->wl_praesenz,
+            'workload_preparation' => $resource->wl_bereitung,
+            'workload_self' => $resource->wl_selbst,
+            'workload_exam' => $resource->wl_pruef,
+            'share_of_grade' => $resource->anteil_note,
+            'compensable' => $resource->ausgleichbar,
+            'compulsory_attendance' => $resource->pflicht,
+            'prerequisites' => $resource->deskriptoren->voraussetzung,
+            'comment' => $resource->deskriptoren->kommentar,
+            'comment_capacity' => $resource->deskriptoren->kommentar_kapazitaet,
+            'comment_wl_compulsory' => $resource->deskriptoren->kommentar_wl_praesenz,
+            'comment_wl_preparation' => $resource->deskriptoren->kommentar_wl_bereitung,
+            'comment_wl_self' => $resource->deskriptoren->kommentar_wl_selbst,
+            'comment_wl_exam' => $resource->deskriptoren->kommentar_wl_pruef,
+            'exam_prerequisites' => $resource->deskriptoren->pruef_vorleistung,
+            'exam_requirements' => $resource->deskriptoren->pruef_leistung,
+            'comment_compulsory_attendance' => $resource->deskriptoren->kommentar_pflicht,
+            'type' => get_class($resource)
+        ];
+    }
+
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        $relationships = $this->addCoursesRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_COURSES));
+
+        return $relationships;
+    }
+
+    private function addCoursesRelationship(array $relationships, \Modulteil $resource, $includeData)
+    {
+        $relationships[self::REL_COURSES] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_COURSES),
+            ],
+        ];
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/ModuleInstitute.php b/lib/classes/JsonApi/Schemas/ModuleInstitute.php
new file mode 100644
index 0000000000000000000000000000000000000000..d5b8e722c771dbeb5341bed961b60bbbf32343ef
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/ModuleInstitute.php
@@ -0,0 +1,67 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class ModuleInstitute extends SchemaProvider
+{
+    const REL_MODULE = 'modules';
+    const REL_INSTITUTE = 'institutes';
+    const TYPE = 'module-institutes';
+
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'name' => (string) $resource->name,
+            'short-name' => (string) $resource->name_kurz,
+            'description' => (string) $resource->beschreibung,
+            'type' => get_class($resource)
+        ];
+    }
+
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        $relationships = $this->addModuleRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_MODULE));
+        $relationships = $this->addInstituteRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_INSTITUTE));
+
+        return $relationships;
+    }
+
+    private function addModuleRelationship(array $relationships, $resource, $includeData)
+    {
+        $relationships[self::REL_MODULE] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_MODULE),
+            ],
+        ];
+
+        if ($includeData) {
+            $relationships[self::REL_MODULE][self::RELATIONSHIP_DATA] = $resource->module;
+        }
+
+        return $relationships;
+    }
+
+    private function addInstituteRelationship(array $relationships, $resource, $includeData)
+    {
+        $relationships[self::REL_INSTITUTE] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_INSTITUTE),
+            ],
+        ];
+
+        if ($includeData) {
+            $relationships[self::REL_INSTITUTE][self::RELATIONSHIP_DATA] = $resource->institute;
+        }
+
+        return $relationships;
+    }
+}
diff --git a/lib/classes/JsonApi/Schemas/Subject.php b/lib/classes/JsonApi/Schemas/Subject.php
new file mode 100644
index 0000000000000000000000000000000000000000..d8fdd61031e78dad1a7f932029a6e1bf8d34e57e
--- /dev/null
+++ b/lib/classes/JsonApi/Schemas/Subject.php
@@ -0,0 +1,61 @@
+<?php
+namespace JsonApi\Schemas;
+
+use Fach;
+use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
+use Neomerx\JsonApi\Schema\Link;
+
+class Subject extends SchemaProvider
+{
+    const REL_DEPARTMENTS = 'departments';
+    const TYPE = 'subjects';
+
+    /**
+     * @param Fach $resource
+     */
+    public function getId($resource): ?string
+    {
+        return $resource->id;
+    }
+
+    /**
+     * @param Fach $resource
+     */
+    public function getAttributes($resource, ContextInterface $context): iterable
+    {
+        return [
+            'name' => (string) $resource->name,
+            'short-name' => (string) $resource->name_kurz,
+            'description' => (string) $resource->beschreibung,
+            'type' => get_class($resource)
+        ];
+    }
+
+    public function getRelationships($resource, ContextInterface $context): iterable
+    {
+        $relationships = [];
+
+        $relationships = $this->addDepartmentsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_DEPARTMENTS));
+
+        return $relationships;
+    }
+
+    private function addDepartmentsRelationship(array $relationships, $resource, $includeData)
+    {
+        $relationships[self::REL_DEPARTMENTS] = [
+            self::RELATIONSHIP_LINKS => [
+                Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_DEPARTMENTS),
+            ],
+        ];
+
+        if ($includeData) {
+            // use institute schema
+            if (!empty($resource->departments)) {
+                $institutes = \Institute::findMany($resource->departments->pluck('id'));
+                $relationships[self::REL_DEPARTMENTS][self::RELATIONSHIP_DATA] = $institutes;
+            }
+        }
+
+        return $relationships;
+    }
+}
diff --git a/lib/models/Institute.php b/lib/models/Institute.php
index 5ecdcefc6c2de578549a00e1f9734774064adba8..e87a40879bca19b05c7742bafbfe439325bf8f70 100644
--- a/lib/models/Institute.php
+++ b/lib/models/Institute.php
@@ -129,6 +129,11 @@ class Institute extends SimpleORMap implements Range
             'order_by'          => 'ORDER BY position',
             'on_delete'         => 'delete',
         ];
+        $config['has_many']['courses_of_study'] = [
+            'class_name'        => Studiengang::class,
+            'assoc_foreign_key' => 'institut_id',
+            'order_by'          => 'ORDER BY name ASC',
+        ];
         $config['additional_fields']['all_status_groups']['get'] = function ($institute) {
             return Statusgruppen::findAllByRangeId($institute->id, true);
         };
diff --git a/lib/models/Modulteil.php b/lib/models/Modulteil.php
index 397fc18de55063d93e3119d6088cac72e4bcefea..3b8f146de6fc2138bd2a38754e8f480976f4e080 100644
--- a/lib/models/Modulteil.php
+++ b/lib/models/Modulteil.php
@@ -379,11 +379,11 @@ class Modulteil extends ModuleManagementModelTreeItem
     /**
      * Retrieves all courses this Modulteil is assigned by its LV-Gruppen.
      * Filtered by a given semester considering the global visibility or the
-     * the visibility for a given user.
+     * visibility for a given user.
      *
      * @param string $semester_id The id of a semester.
      * @param mixed $only_visible Boolean true retrieves only visible courses, false
-     * retrieves all courses. If $only_visible is an user id it depends on the users
+     * retrieves all courses. If $only_visible is a user id it depends on the users
      * status which courses will be retrieved.
      * @return array An array of course data.
      */
diff --git a/lib/models/StgteilVersion.php b/lib/models/StgteilVersion.php
index 5add60b5ca9a12eabb4157a6b662dddc94180332..113875c260533e83af0367942a571a5efd6ed15b 100644
--- a/lib/models/StgteilVersion.php
+++ b/lib/models/StgteilVersion.php
@@ -67,6 +67,14 @@ class StgteilVersion extends ModuleManagementModelTreeItem
             'on_delete' => 'delete',
             'on_store' => 'store'
         ];
+        $config['belongs_to']['start_semester'] = [
+            'class_name'  => Semester::class,
+            'foreign_key' => 'start_sem',
+        ];
+        $config['belongs_to']['end_semester'] = [
+            'class_name'  => Semester::class,
+            'foreign_key' => 'end_sem',
+        ];
 
         $config['additional_fields']['count_abschnitte']['get'] =
             function($version) { return $version->count_abschnitte; };
diff --git a/lib/models/Studiengang.php b/lib/models/Studiengang.php
index ee89abde0e1cad9f884049569f61ada08c92d87c..3c8a10f6e5e96b00180dee9427f7601297722bbd 100644
--- a/lib/models/Studiengang.php
+++ b/lib/models/Studiengang.php
@@ -171,7 +171,7 @@ class Studiengang extends ModuleManagementModelTreeItem
         $config['i18n_fields']['name_kurz']    = true;
         $config['i18n_fields']['beschreibung'] = true;
 
-        $config['default_values']['enroll'] = $GLOBALS['MVV_STUDIENGANG']['ENROLL']['default'];
+        $config['default_values']['enroll'] = Config::get()->MVV_STUDIENGANG['ENROLL']['default'];
 
         parent::configure($config);
     }
@@ -648,7 +648,7 @@ class Studiengang extends ModuleManagementModelTreeItem
         $result = [];
         foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $status) {
             $result[$status['stat']] = [
-                'name' => $GLOBALS['MVV_STUDIENGANG']['STATUS']['values'][$status['stat']]['name'] ?? _('Undefinierter Status'),
+                'name' => Config::get()->MVV_STUDIENGANG['STATUS']['values'][$status['stat']]['name'] ?? _('Undefinierter Status'),
                 'count_objects' => $status['count_objects']
             ];
         }
@@ -845,7 +845,7 @@ class Studiengang extends ModuleManagementModelTreeItem
     {
         $assigned_languages = array();
         $languages_flipped = array_flip($languages);
-        foreach ($GLOBALS['MVV_STUDIENGANG']['SPRACHE']['values'] as $key => $language) {
+        foreach (Config::get()->MVV_STUDIENGANG['SPRACHE']['values'] as $key => $language) {
             if (isset($languages_flipped[$key])) {
                 $language = StudycourseLanguage::find([$this->id, $key]);
                 if (!$language) {
diff --git a/tests/jsonapi/_bootstrap.php b/tests/jsonapi/_bootstrap.php
index 01538aae7d794295173f553f3ca8615776d544a8..2b30aa91ce6dbeec7057a141dc0c93476bfb31c5 100644
--- a/tests/jsonapi/_bootstrap.php
+++ b/tests/jsonapi/_bootstrap.php
@@ -28,6 +28,7 @@ $CACHING_ENABLE = false;
 date_default_timezone_set('Europe/Berlin');
 
 require 'config.inc.php';
+require 'mvv_config.php';
 require_once __DIR__ . '/../../lib/bootstrap-autoload.php';