diff --git a/Gemfile b/Gemfile
index 210ac78fac381f8a6ea3daadfc809f2424252e45..d24d10e7496afef966fbc3d043a64dc4d9caef61 100644
--- a/Gemfile
+++ b/Gemfile
@@ -16,6 +16,7 @@ gem 'mysql2', '~> 0.4.5', group: :mysql
 gem 'pg', '~> 0.18.2', group: :postgres
 
 gem 'rugged', '~> 0.25.1.1'
+gem 'grape-route-helpers', '~> 2.0.0'
 
 gem 'faraday', '~> 0.12'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index f6c1636dfafc36ff71cdbdec67386f3b11821972..1f3d6d2d61829fbf22c9e4a7aa90298c2b2d4349 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -345,6 +345,10 @@ GEM
     grape-entity (0.6.0)
       activesupport
       multi_json (>= 1.3.2)
+    grape-route-helpers (2.0.0)
+      activesupport
+      grape (~> 0.16, >= 0.16.0)
+      rake
     grpc (1.4.0)
       google-protobuf (~> 3.1)
       googleauth (~> 0.5.1)
@@ -981,6 +985,7 @@ DEPENDENCIES
   google-api-client (~> 0.8.6)
   grape (~> 0.19.2)
   grape-entity (~> 0.6.0)
+  grape-route-helpers (~> 2.0.0)
   haml_lint (~> 0.21.0)
   hamlit (~> 2.6.1)
   hashie-forbidden_attributes
diff --git a/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml b/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..837a34bd06777814b3d69b9d94ea2432ed250899
--- /dev/null
+++ b/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml
@@ -0,0 +1,4 @@
+---
+title: Declare related resources into V4 API entities
+merge_request:
+author:
diff --git a/config/initializers/grape_route_helpers_fix.rb b/config/initializers/grape_route_helpers_fix.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d3cf9e453d0a44a32b20a4f3b8a25aa253370f63
--- /dev/null
+++ b/config/initializers/grape_route_helpers_fix.rb
@@ -0,0 +1,35 @@
+if defined?(GrapeRouteHelpers)
+  module GrapeRouteHelpers
+    class DecoratedRoute
+      # GrapeRouteHelpers gem tries to parse the versions
+      # from a string, not supporting Grape `version` array definition.
+      #
+      # Without the following fix, we get this on route helpers generation:
+      #
+      # => undefined method `scan' for ["v3", "v4"]
+      #
+      # 2.0.0 implementation of this method:
+      #
+      # ```
+      # def route_versions
+      #   version_pattern = /[^\[",\]\s]+/
+      #   if route_version
+      #     route_version.scan(version_pattern)
+      #   else
+      #     [nil]
+      #   end
+      # end
+      # ```
+      def route_versions
+        return [nil] if route_version.nil? || route_version.empty?
+
+        if route_version.is_a?(String)
+          version_pattern = /[^\[",\]\s]+/
+          route_version.scan(version_pattern)
+        else
+          route_version
+        end
+      end
+    end
+  end
+end
diff --git a/config/routes/api.rb b/config/routes/api.rb
index 69c8efc151c9c51b2d7e4415d1f262396c019906..ce7a7c889009ee7bb17c087c8fd127005fb0c8a1 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -1,2 +1,2 @@
 API::API.logger Rails.logger
-mount API::API => '/api'
+mount API::API => '/'
diff --git a/doc/api/issues.md b/doc/api/issues.md
index a00a63bad4b1e759105ce290220626f36e3f7145..0e391c75cd35808c6da9d6fbdfebaeba3114e45a 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -356,7 +356,13 @@ Example response:
    "user_notes_count": 1,
    "due_date": null,
    "web_url": "http://example.com/example/example/issues/1",
-   "confidential": false
+   "confidential": false,
+   "_links": {
+      "self": "http://example.com/api/v4/projects/1/issues/2",
+      "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+      "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+      "project": "http://example.com/api/v4/projects/1"
+   }
 }
 ```
 
@@ -418,7 +424,13 @@ Example response:
    "user_notes_count": 0,
    "due_date": null,
    "web_url": "http://example.com/example/example/issues/14",
-   "confidential": false
+   "confidential": false,
+   "_links": {
+      "self": "http://example.com/api/v4/projects/1/issues/2",
+      "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+      "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+      "project": "http://example.com/api/v4/projects/1"
+   }
 }
 ```
 
@@ -481,7 +493,13 @@ Example response:
    "user_notes_count": 0,
    "due_date": "2016-07-22",
    "web_url": "http://example.com/example/example/issues/15",
-   "confidential": false
+   "confidential": false,
+   "_links": {
+      "self": "http://example.com/api/v4/projects/1/issues/2",
+      "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+      "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+      "project": "http://example.com/api/v4/projects/1"
+   }
 }
 ```
 
@@ -567,7 +585,13 @@ Example response:
   },
   "due_date": null,
   "web_url": "http://example.com/example/example/issues/11",
-  "confidential": false
+  "confidential": false,
+  "_links": {
+    "self": "http://example.com/api/v4/projects/1/issues/2",
+    "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+    "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+    "project": "http://example.com/api/v4/projects/1"
+  }
 }
 ```
 
@@ -632,7 +656,13 @@ Example response:
   },
   "due_date": null,
   "web_url": "http://example.com/example/example/issues/11",
-  "confidential": false
+  "confidential": false,
+  "_links": {
+    "self": "http://example.com/api/v4/projects/1/issues/2",
+    "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+    "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+    "project": "http://example.com/api/v4/projects/1"
+  }
 }
 ```
 
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 61ae89a64c084df9bde19eac95ac24047fe923cb..d3f8e509612280995f60c20d9f9ec097a9150ff0 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -99,7 +99,16 @@ Parameters:
       "repository_size": 1038090,
       "lfs_objects_size": 0,
       "job_artifacts_size": 0
-    }
+    },
+    "_links": {
+      "self": "http://example.com/api/v4/projects",
+      "issues": "http://example.com/api/v4/projects/1/issues",
+      "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+      "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+      "labels": "http://example.com/api/v4/projects/1/labels",
+      "events": "http://example.com/api/v4/projects/1/events",
+      "members": "http://example.com/api/v4/projects/1/members"
+    },
   },
   {
     "id": 6,
@@ -168,6 +177,15 @@ Parameters:
       "repository_size": 2066080,
       "lfs_objects_size": 0,
       "job_artifacts_size": 0
+    },
+    "_links": {
+      "self": "http://example.com/api/v4/projects",
+      "issues": "http://example.com/api/v4/projects/1/issues",
+      "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+      "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+      "labels": "http://example.com/api/v4/projects/1/labels",
+      "events": "http://example.com/api/v4/projects/1/events",
+      "members": "http://example.com/api/v4/projects/1/members"
     }
   }
 ]
@@ -257,6 +275,15 @@ Parameters:
       "repository_size": 1038090,
       "lfs_objects_size": 0,
       "job_artifacts_size": 0
+    },
+    "_links": {
+      "self": "http://example.com/api/v4/projects",
+      "issues": "http://example.com/api/v4/projects/1/issues",
+      "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+      "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+      "labels": "http://example.com/api/v4/projects/1/labels",
+      "events": "http://example.com/api/v4/projects/1/events",
+      "members": "http://example.com/api/v4/projects/1/members"
     }
   },
   {
@@ -326,6 +353,15 @@ Parameters:
       "repository_size": 2066080,
       "lfs_objects_size": 0,
       "job_artifacts_size": 0
+    },
+    "_links": {
+      "self": "http://example.com/api/v4/projects",
+      "issues": "http://example.com/api/v4/projects/1/issues",
+      "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+      "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+      "labels": "http://example.com/api/v4/projects/1/labels",
+      "events": "http://example.com/api/v4/projects/1/events",
+      "members": "http://example.com/api/v4/projects/1/members"
     }
   }
 ]
@@ -427,6 +463,15 @@ Parameters:
     "repository_size": 1038090,
     "lfs_objects_size": 0,
     "job_artifacts_size": 0
+  },
+  "_links": {
+    "self": "http://example.com/api/v4/projects",
+    "issues": "http://example.com/api/v4/projects/1/issues",
+    "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+    "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+    "labels": "http://example.com/api/v4/projects/1/labels",
+    "events": "http://example.com/api/v4/projects/1/events",
+    "members": "http://example.com/api/v4/projects/1/members"
   }
 }
 ```
@@ -659,7 +704,16 @@ Example response:
   "shared_with_groups": [],
   "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
-  "request_access_enabled": false
+  "request_access_enabled": false,
+  "_links": {
+    "self": "http://example.com/api/v4/projects",
+    "issues": "http://example.com/api/v4/projects/1/issues",
+    "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+    "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+    "labels": "http://example.com/api/v4/projects/1/labels",
+    "events": "http://example.com/api/v4/projects/1/events",
+    "members": "http://example.com/api/v4/projects/1/members"
+  }
 }
 ```
 
@@ -725,7 +779,16 @@ Example response:
   "shared_with_groups": [],
   "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
-  "request_access_enabled": false
+  "request_access_enabled": false,
+  "_links": {
+    "self": "http://example.com/api/v4/projects",
+    "issues": "http://example.com/api/v4/projects/1/issues",
+    "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+    "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+    "labels": "http://example.com/api/v4/projects/1/labels",
+    "events": "http://example.com/api/v4/projects/1/events",
+    "members": "http://example.com/api/v4/projects/1/members"
+  }
 }
 ```
 
@@ -809,7 +872,16 @@ Example response:
   "shared_with_groups": [],
   "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
-  "request_access_enabled": false
+  "request_access_enabled": false,
+  "_links": {
+    "self": "http://example.com/api/v4/projects",
+    "issues": "http://example.com/api/v4/projects/1/issues",
+    "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+    "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+    "labels": "http://example.com/api/v4/projects/1/labels",
+    "events": "http://example.com/api/v4/projects/1/events",
+    "members": "http://example.com/api/v4/projects/1/members"
+  }
 }
 ```
 
@@ -893,7 +965,16 @@ Example response:
   "shared_with_groups": [],
   "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
-  "request_access_enabled": false
+  "request_access_enabled": false,
+  "_links": {
+    "self": "http://example.com/api/v4/projects",
+    "issues": "http://example.com/api/v4/projects/1/issues",
+    "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+    "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+    "labels": "http://example.com/api/v4/projects/1/labels",
+    "events": "http://example.com/api/v4/projects/1/events",
+    "members": "http://example.com/api/v4/projects/1/members"
+  }
 }
 ```
 
diff --git a/lib/api/api.rb b/lib/api/api.rb
index efcf0976a819d097589ab2a1f48fc7293f0cda01..9a983d31ac680990d37be92deba57d2356d44b78 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -3,6 +3,7 @@ module API
     include APIGuard
 
     allow_access_with_scope :api
+    prefix :api
 
     version %w(v3 v4), using: :path
 
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 1719e9f720547bf0df3c4fe6e8ce365e4bad8020..c165236105f637b57eea5516d00c686c61de477a 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -82,6 +82,38 @@ module API
     end
 
     class Project < Grape::Entity
+      include ::API::Helpers::RelatedResourcesHelpers
+
+      expose :_links do
+        expose :self do |project|
+          expose_url(api_v4_projects_path(id: project.id))
+        end
+
+        expose :issues, if: -> (*args) { issues_available?(*args) } do |project|
+          expose_url(api_v4_projects_issues_path(id: project.id))
+        end
+
+        expose :merge_requests, if: -> (*args) { mrs_available?(*args) } do |project|
+          expose_url(api_v4_projects_merge_requests_path(id: project.id))
+        end
+
+        expose :repo_branches do |project|
+          expose_url(api_v4_projects_repository_branches_path(id: project.id))
+        end
+
+        expose :labels do |project|
+          expose_url(api_v4_projects_labels_path(id: project.id))
+        end
+
+        expose :events do |project|
+          expose_url(api_v4_projects_events_path(id: project.id))
+        end
+
+        expose :members do |project|
+          expose_url(api_v4_projects_members_path(id: project.id))
+        end
+      end
+
       expose :id, :description, :default_branch, :tag_list
       expose :archived?, as: :archived
       expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url
@@ -297,6 +329,26 @@ module API
     end
 
     class Issue < IssueBasic
+      include ::API::Helpers::RelatedResourcesHelpers
+
+      expose :_links do
+        expose :self do |issue|
+          expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid))
+        end
+
+        expose :notes do |issue|
+          expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid))
+        end
+
+        expose :award_emoji do |issue|
+          expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid))
+        end
+
+        expose :project do |issue|
+          expose_url(api_v4_projects_path(id: issue.project_id))
+        end
+      end
+
       expose :subscribed do |issue, options|
         issue.subscribed?(options[:current_user], options[:project] || issue.project)
       end
diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..769cc1457fc1c7311a92ad129f35a4f6d1453b3f
--- /dev/null
+++ b/lib/api/helpers/related_resources_helpers.rb
@@ -0,0 +1,28 @@
+module API
+  module Helpers
+    module RelatedResourcesHelpers
+      include GrapeRouteHelpers::NamedRouteMatcher
+
+      def issues_available?(project, options)
+        available?(:issues, project, options[:current_user])
+      end
+
+      def mrs_available?(project, options)
+        available?(:merge_requests, project, options[:current_user])
+      end
+
+      def expose_url(path)
+        url_options = Rails.application.routes.default_url_options
+        protocol, host, port = url_options.slice(:protocol, :host, :port).values
+
+        URI::HTTP.build(scheme: protocol, host: host, port: port, path: path).to_s
+      end
+
+      private
+
+      def available?(feature, project, current_user)
+        project.feature_available?(feature, current_user)
+      end
+    end
+  end
+end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 14b26f28ebfba27b2898403f346314dc063aafdb..93ebe18508d82cd520e061ce4bb0afbaf48b1fc7 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -112,7 +112,7 @@ module API
       params do
         requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
       end
-      get ":id/issues/:issue_iid" do
+      get ":id/issues/:issue_iid", as: :api_v4_project_issue do
         issue = find_project_issue(params[:issue_iid])
         present issue, with: Entities::Issue, current_user: current_user, project: user_project
       end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 3759250f7f6684ed404e8e6e51c922c84a8f2f88..773f667abe0bcfc120436a65733f5a531a6b51e5 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -259,11 +259,40 @@ module API
         expose :job_events, as: :build_events
       end
 
-      class Issue < ::API::Entities::Issue
+      class ProjectEntity < Grape::Entity
+        expose :id, :iid
+        expose(:project_id) { |entity| entity&.project.try(:id) }
+        expose :title, :description
+        expose :state, :created_at, :updated_at
+      end
+
+      class IssueBasic < ProjectEntity
+        expose :label_names, as: :labels
+        expose :milestone, using: ::API::Entities::Milestone
+        expose :assignees, :author, using: ::API::Entities::UserBasic
+
+        expose :assignee, using: ::API::Entities::UserBasic do |issue, options|
+          issue.assignees.first
+        end
+
+        expose :user_notes_count
+        expose :upvotes, :downvotes
+        expose :due_date
+        expose :confidential
+
+        expose :web_url do |issue, options|
+          Gitlab::UrlBuilder.build(issue)
+        end
+      end
+
+      class Issue < IssueBasic
         unexpose :assignees
         expose :assignee do |issue, options|
           ::API::Entities::UserBasic.represent(issue.assignees.first, options)
         end
+        expose :subscribed do |issue, options|
+          issue.subscribed?(options[:current_user], options[:project] || issue.project)
+        end
       end
     end
   end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 9837fedb5229d71cbcb8df57fc259a6ebce0bd2b..ff4fc802176bf05726bc6f037a4e7a6c81b2b7da 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -693,6 +693,19 @@ describe API::Issues do
       expect(json_response['confidential']).to be_falsy
     end
 
+    context 'links exposure' do
+      it 'exposes related resources full URIs' do
+        get api("/projects/#{project.id}/issues/#{issue.iid}", user)
+
+        links = json_response['_links']
+
+        expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}")
+        expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes")
+        expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji")
+        expect(links['project']).to end_with("/api/v4/projects/#{project.id}")
+      end
+    end
+
     it "returns a project issue by internal id" do
       get api("/projects/#{project.id}/issues/#{issue.iid}", user)
 
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 457f64cc88c75758af822e3cffc4b7e03285f631..79e7e1a95dffa7cf76fa5932ee25352f12538968 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -815,6 +815,38 @@ describe API::Projects do
         expect(json_response).not_to include("import_error")
       end
 
+      context 'links exposure' do
+        it 'exposes related resources full URIs' do
+          get api("/projects/#{project.id}", user)
+
+          links = json_response['_links']
+
+          expect(links['self']).to end_with("/api/v4/projects/#{project.id}")
+          expect(links['issues']).to end_with("/api/v4/projects/#{project.id}/issues")
+          expect(links['merge_requests']).to end_with("/api/v4/projects/#{project.id}/merge_requests")
+          expect(links['repo_branches']).to end_with("/api/v4/projects/#{project.id}/repository/branches")
+          expect(links['labels']).to end_with("/api/v4/projects/#{project.id}/labels")
+          expect(links['events']).to end_with("/api/v4/projects/#{project.id}/events")
+          expect(links['members']).to end_with("/api/v4/projects/#{project.id}/members")
+        end
+
+        it 'filters related URIs when their feature is not enabled' do
+          project = create(:empty_project, :public,
+                           :merge_requests_disabled,
+                           :issues_disabled,
+                           creator_id: user.id,
+                           namespace: user.namespace)
+
+          get api("/projects/#{project.id}", user)
+
+          links = json_response['_links']
+
+          expect(links.has_key?('merge_requests')).to be_falsy
+          expect(links.has_key?('issues')).to be_falsy
+          expect(links['self']).to end_with("/api/v4/projects/#{project.id}")
+        end
+      end
+
       describe 'permissions' do
         context 'all projects' do
           before do