diff --git a/doc/api/README.md b/doc/api/README.md
index 95e7a457848b01ba22d0654bc6dc9e8e97ff2560..a888c0ebb4ea78dbe528729d9dbe0876b92c2bf8 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -29,7 +29,8 @@ following locations:
 - [Keys](keys.md)
 - [Labels](labels.md)
 - [Merge Requests](merge_requests.md)
-- [Milestones](milestones.md)
+- [Project milestones](milestones.md)
+- [Group milestones](group_milestones.md)
 - [Namespaces](namespaces.md)
 - [Notes](notes.md) (comments)
 - [Notification settings](notification_settings.md)
diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md
new file mode 100644
index 0000000000000000000000000000000000000000..086fba7e91d8f1ef1f1d077adc00b5384b9b846e
--- /dev/null
+++ b/doc/api/group_milestones.md
@@ -0,0 +1,120 @@
+# Group milestones API
+
+## List group milestones
+
+Returns a list of group milestones.
+
+```
+GET /groups/:id/milestones
+GET /groups/:id/milestones?iids=42
+GET /groups/:id/milestones?iids[]=42&iids[]=43
+GET /groups/:id/milestones?state=active
+GET /groups/:id/milestones?state=closed
+GET /groups/:id/milestones?search=version
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `iids` | Array[integer] | optional | Return only the milestones having the given `iids` |
+| `state` | string | optional | Return only `active` or `closed` milestones` |
+| `search` | string | optional | Return only milestones with a title or description matching the provided string |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/milestones
+```
+
+Example Response:
+
+```json
+[
+  {
+    "id": 12,
+    "iid": 3,
+    "group_id": 16,
+    "title": "10.0",
+    "description": "Version",
+    "due_date": "2013-11-29",
+    "start_date": "2013-11-10",
+    "state": "active",
+    "updated_at": "2013-10-02T09:24:18Z",
+    "created_at": "2013-10-02T09:24:18Z"
+  }
+]
+```
+
+
+## Get single milestone
+
+Gets a single group milestone.
+
+```
+GET /groups/:id/milestones/:milestone_id
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of the group milestone
+
+## Create new milestone
+
+Creates a new group milestone.
+
+```
+POST /groups/:id/milestones
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `title` (required) - The title of an milestone
+- `description` (optional) - The description of the milestone
+- `due_date` (optional) - The due date of the milestone
+- `start_date` (optional) - The start date of the milestone
+
+## Edit milestone
+
+Updates an existing group milestone.
+
+```
+PUT /groups/:id/milestones/:milestone_id
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of a group milestone
+- `title` (optional) - The title of a milestone
+- `description` (optional) - The description of a milestone
+- `due_date` (optional) - The due date of the milestone
+- `start_date` (optional) - The start date of the milestone
+- `state_event` (optional) - The state event of the milestone (close|activate)
+
+## Get all issues assigned to a single milestone
+
+Gets all issues assigned to a single group milestone.
+
+```
+GET /groups/:id/milestones/:milestone_id/issues
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of a group milestone
+
+## Get all merge requests assigned to a single milestone
+
+Gets all merge requests assigned to a single group milestone.
+
+```
+GET /groups/:id/milestones/:milestone_id/merge_requests
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of a group milestone
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 9a983d31ac680990d37be92deba57d2356d44b78..3bdafa3edc108a31ebd6f69929dc01c74c3c828f 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -110,7 +110,8 @@ module API
     mount ::API::Members
     mount ::API::MergeRequestDiffs
     mount ::API::MergeRequests
-    mount ::API::Milestones
+    mount ::API::ProjectMilestones
+    mount ::API::GroupMilestones
     mount ::API::Namespaces
     mount ::API::Notes
     mount ::API::NotificationSettings
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index c165236105f637b57eea5516d00c686c61de477a..5cdc441e8cbd9664f242651084db300ea2570368 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -301,8 +301,8 @@ module API
 
     class Milestone < Grape::Entity
       expose :id, :iid
-      expose(:project_id) { |entity| entity&.project_id }
-      expose(:group_id) { |entity| entity&.group_id }
+      expose :project_id, if: -> (entity, options) { entity&.project_id }
+      expose :group_id, if: -> (entity, options) { entity&.group_id }
       expose :title, :description
       expose :state, :created_at, :updated_at
       expose :due_date
diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b85eb59dc0a98166d532a5f279f90e8d9dbf5f85
--- /dev/null
+++ b/lib/api/group_milestones.rb
@@ -0,0 +1,85 @@
+module API
+  class GroupMilestones < Grape::API
+    include MilestoneResponses
+    include PaginationParams
+
+    before do
+      authenticate!
+    end
+
+    params do
+      requires :id, type: String, desc: 'The ID of a group'
+    end
+    resource :groups, requirements: { id: %r{[^/]+} } do
+      desc 'Get a list of group milestones' do
+        success Entities::Milestone
+      end
+      params do
+        use :list_params
+      end
+      get ":id/milestones" do
+        list_milestones_for(user_group)
+      end
+
+      desc 'Get a single group milestone' do
+        success Entities::Milestone
+      end
+      params do
+        requires :milestone_id, type: Integer, desc: 'The ID of a group milestone'
+      end
+      get ":id/milestones/:milestone_id" do
+        authorize! :read_group, user_group
+
+        get_milestone_for(user_group)
+      end
+
+      desc 'Create a new group milestone' do
+        success Entities::Milestone
+      end
+      params do
+        requires :title, type: String, desc: 'The title of the milestone'
+        use :optional_params
+      end
+      post ":id/milestones" do
+        authorize! :admin_milestones, user_group
+
+        create_milestone_for(user_group)
+      end
+
+      desc 'Update an existing group milestone' do
+        success Entities::Milestone
+      end
+      params do
+        use :update_params
+      end
+      put ":id/milestones/:milestone_id" do
+        authorize! :admin_milestones, user_group
+
+        update_milestone_for(user_group)
+      end
+
+      desc 'Get all issues for a single group milestone' do
+        success Entities::IssueBasic
+      end
+      params do
+        requires :milestone_id, type: Integer, desc: 'The ID of a group milestone'
+        use :pagination
+      end
+      get ":id/milestones/:milestone_id/issues" do
+        milestone_issuables_for(user_group, :issue)
+      end
+
+      desc 'Get all merge requests for a single group milestone' do
+        detail 'This feature was introduced in GitLab 9.'
+        success Entities::MergeRequestBasic
+      end
+      params do
+        requires :milestone_id, type: Integer, desc: 'The ID of a group milestone'
+        use :pagination
+      end
+      get ':id/milestones/:milestone_id/merge_requests' do
+        milestone_issuables_for(user_group, :merge_request)
+      end
+    end
+  end
+end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 0f4791841d26eeb04dbadfdab014a55095c307c4..57e3e93500ff58e6aaa68b5c4d47e1c97bc63abb 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -25,6 +25,10 @@ module API
       initial_current_user != current_user
     end
 
+    def user_group
+      @group ||= find_group!(params[:id])
+    end
+
     def user_project
       @project ||= find_project!(params[:id])
     end
diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ef09d9505d25a5a019cf41d71f4a785a720b78b7
--- /dev/null
+++ b/lib/api/milestone_responses.rb
@@ -0,0 +1,98 @@
+module API
+  module MilestoneResponses
+    extend ActiveSupport::Concern
+
+    included do
+      helpers do
+        params :optional_params do
+          optional :description, type: String, desc: 'The description of the milestone'
+          optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
+          optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
+        end
+
+        params :list_params do
+          optional :state, type: String, values: %w[active closed all], default: 'all',
+                           desc: 'Return "active", "closed", or "all" milestones'
+          optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones'
+          optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
+          use :pagination
+        end
+
+        params :update_params do
+          requires :milestone_id, type: Integer, desc: 'The milestone ID number'
+          optional :title, type: String, desc: 'The title of the milestone'
+          optional :state_event, type: String, values: %w[close activate],
+                                 desc: 'The state event of the milestone '
+          use :optional_params
+          at_least_one_of :title, :description, :due_date, :state_event
+        end
+
+        def list_milestones_for(parent)
+          milestones = parent.milestones
+          milestones = Milestone.filter_by_state(milestones, params[:state])
+          milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
+          milestones = filter_by_search(milestones, params[:search]) if params[:search]
+
+          present paginate(milestones), with: Entities::Milestone
+        end
+
+        def get_milestone_for(parent)
+          milestone = parent.milestones.find(params[:milestone_id])
+          present milestone, with: Entities::Milestone
+        end
+
+        def create_milestone_for(parent)
+          milestone = ::Milestones::CreateService.new(parent, current_user, declared_params).execute
+
+          if milestone.valid?
+            present milestone, with: Entities::Milestone
+          else
+            render_api_error!("Failed to create milestone #{milestone.errors.messages}", 400)
+          end
+        end
+
+        def update_milestone_for(parent)
+          milestone = parent.milestones.find(params.delete(:milestone_id))
+
+          milestone_params = declared_params(include_missing: false)
+          milestone = ::Milestones::UpdateService.new(parent, current_user, milestone_params).execute(milestone)
+
+          if milestone.valid?
+            present milestone, with: Entities::Milestone
+          else
+            render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400)
+          end
+        end
+
+        def milestone_issuables_for(parent, type)
+          milestone = parent.milestones.find(params[:milestone_id])
+
+          finder_klass, entity = get_finder_and_entity(type)
+
+          params = build_finder_params(milestone, parent)
+
+          issuables = finder_klass.new(current_user, params).execute
+          present paginate(issuables), with: entity, current_user: current_user
+        end
+
+        def build_finder_params(milestone, parent)
+          finder_params = { milestone_title: milestone.title, sort: 'label_priority' }
+
+          if parent.is_a?(Group)
+            finder_params.merge(group_id: parent.id)
+          else
+            finder_params.merge(project_id: parent.id)
+          end
+        end
+
+        def get_finder_and_entity(type)
+          if type == :issue
+            [IssuesFinder, Entities::IssueBasic]
+          else
+            [MergeRequestsFinder, Entities::MergeRequestBasic]
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
deleted file mode 100644
index 3541d3c95fb616339cb54624116a2aa8d17352cf..0000000000000000000000000000000000000000
--- a/lib/api/milestones.rb
+++ /dev/null
@@ -1,154 +0,0 @@
-module API
-  class Milestones < Grape::API
-    include PaginationParams
-
-    before { authenticate! }
-
-    helpers do
-      def filter_milestones_state(milestones, state)
-        case state
-        when 'active' then milestones.active
-        when 'closed' then milestones.closed
-        else milestones
-        end
-      end
-
-      params :optional_params do
-        optional :description, type: String, desc: 'The description of the milestone'
-        optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
-        optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
-      end
-    end
-
-    params do
-      requires :id, type: String, desc: 'The ID of a project'
-    end
-    resource :projects, requirements: { id: %r{[^/]+} } do
-      desc 'Get a list of project milestones' do
-        success Entities::Milestone
-      end
-      params do
-        optional :state, type: String, values: %w[active closed all], default: 'all',
-                         desc: 'Return "active", "closed", or "all" milestones'
-        optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones'
-        optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
-        use :pagination
-      end
-      get ":id/milestones" do
-        authorize! :read_milestone, user_project
-
-        milestones = user_project.milestones
-        milestones = filter_milestones_state(milestones, params[:state])
-        milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
-        milestones = filter_by_search(milestones, params[:search]) if params[:search]
-
-        present paginate(milestones), with: Entities::Milestone
-      end
-
-      desc 'Get a single project milestone' do
-        success Entities::Milestone
-      end
-      params do
-        requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
-      end
-      get ":id/milestones/:milestone_id" do
-        authorize! :read_milestone, user_project
-
-        milestone = user_project.milestones.find(params[:milestone_id])
-        present milestone, with: Entities::Milestone
-      end
-
-      desc 'Create a new project milestone' do
-        success Entities::Milestone
-      end
-      params do
-        requires :title, type: String, desc: 'The title of the milestone'
-        use :optional_params
-      end
-      post ":id/milestones" do
-        authorize! :admin_milestone, user_project
-
-        milestone = ::Milestones::CreateService.new(user_project, current_user, declared_params).execute
-
-        if milestone.valid?
-          present milestone, with: Entities::Milestone
-        else
-          render_api_error!("Failed to create milestone #{milestone.errors.messages}", 400)
-        end
-      end
-
-      desc 'Update an existing project milestone' do
-        success Entities::Milestone
-      end
-      params do
-        requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
-        optional :title, type: String, desc: 'The title of the milestone'
-        optional :state_event, type: String, values: %w[close activate],
-                               desc: 'The state event of the milestone '
-        use :optional_params
-        at_least_one_of :title, :description, :due_date, :state_event
-      end
-      put ":id/milestones/:milestone_id" do
-        authorize! :admin_milestone, user_project
-        milestone = user_project.milestones.find(params.delete(:milestone_id))
-
-        milestone_params = declared_params(include_missing: false)
-        milestone = ::Milestones::UpdateService.new(user_project, current_user, milestone_params).execute(milestone)
-
-        if milestone.valid?
-          present milestone, with: Entities::Milestone
-        else
-          render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400)
-        end
-      end
-
-      desc 'Get all issues for a single project milestone' do
-        success Entities::IssueBasic
-      end
-      params do
-        requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
-        use :pagination
-      end
-      get ":id/milestones/:milestone_id/issues" do
-        authorize! :read_milestone, user_project
-
-        milestone = user_project.milestones.find(params[:milestone_id])
-
-        finder_params = {
-          project_id: user_project.id,
-          milestone_title: milestone.title,
-          sort: 'label_priority'
-        }
-
-        issues = IssuesFinder.new(current_user, finder_params).execute
-        present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
-      end
-
-      desc 'Get all merge requests for a single project milestone' do
-        detail 'This feature was introduced in GitLab 9.'
-        success Entities::MergeRequestBasic
-      end
-      params do
-        requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
-        use :pagination
-      end
-      get ':id/milestones/:milestone_id/merge_requests' do
-        authorize! :read_milestone, user_project
-
-        milestone = user_project.milestones.find(params[:milestone_id])
-
-        finder_params = {
-          project_id: user_project.id,
-          milestone_title: milestone.title,
-          sort: 'label_priority'
-        }
-
-        merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute
-        present paginate(merge_requests),
-          with: Entities::MergeRequestBasic,
-          current_user: current_user,
-          project: user_project
-      end
-    end
-  end
-end
diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb
new file mode 100644
index 0000000000000000000000000000000000000000..451998c726ac721039edecf1424a0163704b692f
--- /dev/null
+++ b/lib/api/project_milestones.rb
@@ -0,0 +1,91 @@
+module API
+  class ProjectMilestones < Grape::API
+    include PaginationParams
+    include MilestoneResponses
+
+    before do
+      authenticate!
+    end
+
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
+    resource :projects, requirements: { id: %r{[^/]+} } do
+      desc 'Get a list of project milestones' do
+        success Entities::Milestone
+      end
+      params do
+        use :list_params
+      end
+      get ":id/milestones" do
+        authorize! :read_milestone, user_project
+
+        list_milestones_for(user_project)
+      end
+
+      desc 'Get a single project milestone' do
+        success Entities::Milestone
+      end
+      params do
+        requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+      end
+      get ":id/milestones/:milestone_id" do
+        authorize! :read_milestone, user_project
+
+        get_milestone_for(user_project)
+      end
+
+      desc 'Create a new project milestone' do
+        success Entities::Milestone
+      end
+      params do
+        requires :title, type: String, desc: 'The title of the milestone'
+        use :optional_params
+      end
+      post ":id/milestones" do
+        authorize! :admin_milestone, user_project
+
+        create_milestone_for(user_project)
+      end
+
+      desc 'Update an existing project milestone' do
+        success Entities::Milestone
+      end
+      params do
+        use :update_params
+      end
+      put ":id/milestones/:milestone_id" do
+        authorize! :admin_milestone, user_project
+
+        update_milestone_for(user_project)
+      end
+
+      desc 'Get all issues for a single project milestone' do
+        success Entities::IssueBasic
+      end
+      params do
+        requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+        use :pagination
+      end
+      get ":id/milestones/:milestone_id/issues" do
+        authorize! :read_milestone, user_project
+
+        milestone_issuables_for(user_project, :issue)
+      end
+
+      desc 'Get all merge requests for a single project milestone' do
+        detail 'This feature was introduced in GitLab 9.'
+        success Entities::MergeRequestBasic
+      end
+      params do
+        requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+        use :pagination
+      end
+      get ':id/milestones/:milestone_id/merge_requests' do
+        authorize! :read_milestone, user_project
+
+        milestone_issuables_for(user_project, :merge_request)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b24658771f8db5cccd6f842a94c0d4bbeda9d17
--- /dev/null
+++ b/spec/requests/api/group_milestones_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe API::GroupMilestones do
+  let(:user) { create(:user) }
+  let(:group) { create(:group, :private) }
+  let(:project) { create(:empty_project, namespace: group) }
+  let!(:group_member) { create(:group_member, group: group, user: user) }
+  let!(:closed_milestone) { create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone') }
+  let!(:milestone) { create(:milestone, group: group, title: 'version2', description: 'open milestone') }
+
+  it_behaves_like 'group and project milestones', "/groups/:id/milestones"  do
+    let(:route) { "/groups/#{group.id}/milestones" }
+  end
+
+  def setup_for_group
+    context_group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+    context_group.add_developer(user)
+    public_project.update(namespace: context_group)
+    context_group.reload
+  end
+end
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fe8fdbfd7e429b3be9827d2267b86023fecf00e7
--- /dev/null
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe API::ProjectMilestones do
+  let(:user) { create(:user) }
+  let!(:project) { create(:empty_project, namespace: user.namespace ) }
+  let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
+  let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
+
+  before do
+    project.team << [user, :developer]
+  end
+
+  it_behaves_like 'group and project milestones', "/projects/:id/milestones"  do
+    let(:route) { "/projects/#{project.id}/milestones" }
+  end
+
+  describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
+    it 'creates an activity event when an milestone is closed' do
+      expect(Event).to receive(:create)
+
+      put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+          state_event: 'close'
+    end
+  end
+end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/support/api/milestones_shared_examples.rb
similarity index 63%
rename from spec/requests/api/milestones_spec.rb
rename to spec/support/api/milestones_shared_examples.rb
index ab5ea3e8f2c42dba76c07240d33286963d116cbb..480e7d5151f154ef27a0146399b7695952d541b8 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -1,21 +1,14 @@
-require 'spec_helper'
-
-describe API::Milestones do
-  let(:user) { create(:user) }
-  let!(:project) { create(:empty_project, namespace: user.namespace ) }
-  let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
-  let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
+shared_examples_for 'group and project milestones' do |route_definition|
+  let(:resource_route) { "#{route}/#{milestone.id}" }
   let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
   let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
   let(:label_3) { create(:label, title: 'label_3', project: project) }
+  let(:merge_request) { create(:merge_request, source_project: project) }
+  let(:another_merge_request) { create(:merge_request, :simple, source_project: project) }
 
-  before do
-    project.team << [user, :developer]
-  end
-
-  describe 'GET /projects/:id/milestones' do
-    it 'returns project milestones' do
-      get api("/projects/#{project.id}/milestones", user)
+  describe "GET #{route_definition}" do
+    it 'returns milestones list' do
+      get api(route, user)
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
@@ -24,13 +17,13 @@ describe API::Milestones do
     end
 
     it 'returns a 401 error if user not authenticated' do
-      get api("/projects/#{project.id}/milestones")
+      get api(route)
 
       expect(response).to have_http_status(401)
     end
 
     it 'returns an array of active milestones' do
-      get api("/projects/#{project.id}/milestones?state=active", user)
+      get api("#{route}/?state=active", user)
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
@@ -40,7 +33,7 @@ describe API::Milestones do
     end
 
     it 'returns an array of closed milestones' do
-      get api("/projects/#{project.id}/milestones?state=closed", user)
+      get api("#{route}/?state=closed", user)
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
@@ -50,9 +43,9 @@ describe API::Milestones do
     end
 
     it 'returns an array of milestones specified by iids' do
-      other_milestone = create(:milestone, project: project)
+      other_milestone = create(:milestone, project: try(:project), group: try(:group))
 
-      get api("/projects/#{project.id}/milestones", user), iids: [closed_milestone.iid, other_milestone.iid]
+      get api(route, user), iids: [closed_milestone.iid, other_milestone.iid]
 
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -61,25 +54,15 @@ describe API::Milestones do
     end
 
     it 'does not return any milestone if none found' do
-      get api("/projects/#{project.id}/milestones", user), iids: [Milestone.maximum(:iid).succ]
+      get api(route, user), iids: [Milestone.maximum(:iid).succ]
 
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
       expect(json_response.length).to eq(0)
     end
-  end
-
-  describe 'GET /projects/:id/milestones/:milestone_id' do
-    it 'returns a project milestone by id' do
-      get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
-
-      expect(response).to have_http_status(200)
-      expect(json_response['title']).to eq(milestone.title)
-      expect(json_response['iid']).to eq(milestone.iid)
-    end
 
-    it 'returns a project milestone by iids array' do
-      get api("/projects/#{project.id}/milestones?iids=#{closed_milestone.iid}", user)
+    it 'returns a milestone by iids array' do
+      get api("#{route}?iids=#{closed_milestone.iid}", user)
 
       expect(response.status).to eq 200
       expect(response).to include_pagination_headers
@@ -89,8 +72,8 @@ describe API::Milestones do
       expect(json_response.first['id']).to eq closed_milestone.id
     end
 
-    it 'returns a project milestone by searching for title' do
-      get api("/projects/#{project.id}/milestones", user), search: 'version2'
+    it 'returns a milestone by searching for title' do
+      get api(route, user), search: 'version2'
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
@@ -99,8 +82,8 @@ describe API::Milestones do
       expect(json_response.first['id']).to eq milestone.id
     end
 
-    it 'returns a project milestones by searching for description' do
-      get api("/projects/#{project.id}/milestones", user), search: 'open'
+    it 'returns a milestones by searching for description' do
+      get api(route, user), search: 'open'
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
@@ -110,9 +93,17 @@ describe API::Milestones do
     end
   end
 
-  describe 'GET /projects/:id/milestones/:milestone_id' do
-    it 'returns a project milestone by id' do
-      get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
+  describe "GET #{route_definition}/:milestone_id" do
+    it 'returns a milestone by id' do
+      get api(resource_route, user)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['title']).to eq(milestone.title)
+      expect(json_response['iid']).to eq(milestone.iid)
+    end
+
+    it 'returns a milestone by id' do
+      get api(resource_route, user)
 
       expect(response).to have_http_status(200)
       expect(json_response['title']).to eq(milestone.title)
@@ -120,29 +111,29 @@ describe API::Milestones do
     end
 
     it 'returns 401 error if user not authenticated' do
-      get api("/projects/#{project.id}/milestones/#{milestone.id}")
+      get api(resource_route)
 
       expect(response).to have_http_status(401)
     end
 
     it 'returns a 404 error if milestone id not found' do
-      get api("/projects/#{project.id}/milestones/1234", user)
+      get api("#{route}/1234", user)
 
       expect(response).to have_http_status(404)
     end
   end
 
-  describe 'POST /projects/:id/milestones' do
-    it 'creates a new project milestone' do
-      post api("/projects/#{project.id}/milestones", user), title: 'new milestone'
+  describe "POST #{route_definition}" do
+    it 'creates a new milestone' do
+      post api(route, user), title: 'new milestone'
 
       expect(response).to have_http_status(201)
       expect(json_response['title']).to eq('new milestone')
       expect(json_response['description']).to be_nil
     end
 
-    it 'creates a new project milestone with description and dates' do
-      post api("/projects/#{project.id}/milestones", user),
+    it 'creates a new milestone with description and dates' do
+      post api(route, user),
         title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02'
 
       expect(response).to have_http_status(201)
@@ -152,20 +143,20 @@ describe API::Milestones do
     end
 
     it 'returns a 400 error if title is missing' do
-      post api("/projects/#{project.id}/milestones", user)
+      post api(route, user)
 
       expect(response).to have_http_status(400)
     end
 
     it 'returns a 400 error if params are invalid (duplicate title)' do
-      post api("/projects/#{project.id}/milestones", user),
+      post api(route, user),
         title: milestone.title, description: 'release', due_date: '2013-03-02'
 
       expect(response).to have_http_status(400)
     end
 
-    it 'creates a new project with reserved html characters' do
-      post api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2'
+    it 'creates a new milestone with reserved html characters' do
+      post api(route, user), title: 'foo & bar 1.1 -> 2.2'
 
       expect(response).to have_http_status(201)
       expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2')
@@ -173,9 +164,9 @@ describe API::Milestones do
     end
   end
 
-  describe 'PUT /projects/:id/milestones/:milestone_id' do
-    it 'updates a project milestone' do
-      put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+  describe "PUT #{route_definition}/:milestone_id" do
+    it 'updates a milestone' do
+      put api(resource_route, user),
         title: 'updated title'
 
       expect(response).to have_http_status(200)
@@ -185,23 +176,21 @@ describe API::Milestones do
     it 'removes a due date if nil is passed' do
       milestone.update!(due_date: "2016-08-05")
 
-      put api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil
+      put api(resource_route, user), due_date: nil
 
       expect(response).to have_http_status(200)
       expect(json_response['due_date']).to be_nil
     end
 
     it 'returns a 404 error if milestone id not found' do
-      put api("/projects/#{project.id}/milestones/1234", user),
+      put api("#{route}/1234", user),
         title: 'updated title'
 
       expect(response).to have_http_status(404)
     end
-  end
 
-  describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do
-    it 'updates a project milestone' do
-      put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+    it 'closes milestone' do
+      put api(resource_route, user),
         state_event: 'close'
       expect(response).to have_http_status(200)
 
@@ -209,21 +198,14 @@ describe API::Milestones do
     end
   end
 
-  describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
-    it 'creates an activity event when an milestone is closed' do
-      expect(Event).to receive(:create)
-
-      put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
-          state_event: 'close'
-    end
-  end
+  describe "GET #{route_definition}/:milestone_id/issues" do
+    let(:issues_route) { "#{route}/#{milestone.id}/issues" }
 
-  describe 'GET /projects/:id/milestones/:milestone_id/issues' do
     before do
       milestone.issues << create(:issue, project: project)
     end
-    it 'returns project issues for a particular milestone' do
-      get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+    it 'returns issues for a particular milestone' do
+      get api(issues_route, user)
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers
@@ -231,12 +213,12 @@ describe API::Milestones do
       expect(json_response.first['milestone']['title']).to eq(milestone.title)
     end
 
-    it 'returns project issues sorted by label priority' do
+    it 'returns issues sorted by label priority' do
       issue_1 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_3])
       issue_2 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_1])
       issue_3 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_2])
 
-      get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+      get api(issues_route, user)
 
       expect(json_response.first['id']).to eq(issue_2.id)
       expect(json_response.second['id']).to eq(issue_3.id)
@@ -244,44 +226,58 @@ describe API::Milestones do
     end
 
     it 'matches V4 response schema for a list of issues' do
-      get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+      get api(issues_route, user)
 
       expect(response).to have_http_status(200)
       expect(response).to match_response_schema('public_api/v4/issues')
     end
 
     it 'returns a 401 error if user not authenticated' do
-      get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
+      get api(issues_route)
 
       expect(response).to have_http_status(401)
     end
 
     describe 'confidential issues' do
-      let(:public_project) { create(:empty_project, :public) }
-      let(:milestone) { create(:milestone, project: public_project) }
-      let(:issue) { create(:issue, project: public_project) }
-      let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+      let!(:public_project) { create(:empty_project, :public) }
+      let!(:context_group) { try(:group) }
+      let!(:milestone) do
+        context_group ? create(:milestone, group: context_group) : create(:milestone, project: public_project)
+      end
+      let!(:issue) { create(:issue, project: public_project) }
+      let!(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+      let!(:issues_route) do
+        if context_group
+          "#{route}/#{milestone.id}/issues"
+        else
+          "/projects/#{public_project.id}/milestones/#{milestone.id}/issues"
+        end
+      end
 
       before do
+        # Add public project to the group in context
+        setup_for_group if context_group
+
         public_project.team << [user, :developer]
         milestone.issues << issue << confidential_issue
       end
 
       it 'returns confidential issues to team members' do
-        get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
+        get api(issues_route, user)
 
         expect(response).to have_http_status(200)
         expect(response).to include_pagination_headers
         expect(json_response).to be_an Array
-        expect(json_response.size).to eq(2)
+        # 2 for projects, 3 for group(which has another project with an issue)
+        expect(json_response.size).to be_between(2, 3)
         expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
       end
 
       it 'does not return confidential issues to team members with guest role' do
         member = create(:user)
-        project.team << [member, :guest]
+        public_project.team << [member, :guest]
 
-        get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
+        get api(issues_route, member)
 
         expect(response).to have_http_status(200)
         expect(response).to include_pagination_headers
@@ -291,7 +287,7 @@ describe API::Milestones do
       end
 
       it 'does not return confidential issues to regular users' do
-        get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user))
+        get api(issues_route, create(:user))
 
         expect(response).to have_http_status(200)
         expect(response).to include_pagination_headers
@@ -304,30 +300,30 @@ describe API::Milestones do
         issue.labels << label_2
         confidential_issue.labels << label_1
 
-        get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
+        get api(issues_route, user)
 
         expect(response).to have_http_status(200)
         expect(response).to include_pagination_headers
         expect(json_response).to be_an Array
-        expect(json_response.size).to eq(2)
+        # 2 for projects, 3 for group(which has another project with an issue)
+        expect(json_response.size).to be_between(2, 3)
         expect(json_response.first['id']).to eq(confidential_issue.id)
         expect(json_response.second['id']).to eq(issue.id)
       end
     end
   end
 
-  describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do
-    let(:merge_request) { create(:merge_request, source_project: project) }
-    let(:another_merge_request) { create(:merge_request, :simple, source_project: project) }
+  describe "GET #{route_definition}/:milestone_id/merge_requests" do
+    let(:merge_requests_route) { "#{route}/#{milestone.id}/merge_requests" }
 
     before do
       milestone.merge_requests << merge_request
     end
 
-    it 'returns project merge_requests for a particular milestone' do
+    it 'returns merge_requests for a particular milestone' do
       # eager-load another_merge_request
       another_merge_request
-      get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
+      get api(merge_requests_route, user)
 
       expect(response).to have_http_status(200)
       expect(json_response).to be_an Array
@@ -336,12 +332,12 @@ describe API::Milestones do
       expect(json_response.first['milestone']['title']).to eq(milestone.title)
     end
 
-    it 'returns project merge_requests sorted by label priority' do
+    it 'returns merge_requests sorted by label priority' do
       merge_request_1 = create(:labeled_merge_request, source_branch: 'branch_1', source_project: project, milestone: milestone, labels: [label_2])
       merge_request_2 = create(:labeled_merge_request, source_branch: 'branch_2', source_project: project, milestone: milestone, labels: [label_1])
       merge_request_3 = create(:labeled_merge_request, source_branch: 'branch_3', source_project: project, milestone: milestone, labels: [label_3])
 
-      get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
+      get api(merge_requests_route, user)
 
       expect(json_response.first['id']).to eq(merge_request_2.id)
       expect(json_response.second['id']).to eq(merge_request_1.id)
@@ -349,20 +345,22 @@ describe API::Milestones do
     end
 
     it 'returns a 404 error if milestone id not found' do
-      get api("/projects/#{project.id}/milestones/1234/merge_requests", user)
+      not_found_route = "#{route}/1234/merge_requests"
+
+      get api(not_found_route, user)
 
       expect(response).to have_http_status(404)
     end
 
     it 'returns a 404 if the user has no access to the milestone' do
       new_user = create :user
-      get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", new_user)
+      get api(merge_requests_route, new_user)
 
       expect(response).to have_http_status(404)
     end
 
     it 'returns a 401 error if user not authenticated' do
-      get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests")
+      get api(merge_requests_route)
 
       expect(response).to have_http_status(401)
     end
@@ -372,7 +370,7 @@ describe API::Milestones do
       another_merge_request.labels << label_1
       merge_request.labels << label_2
 
-      get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
+      get api(merge_requests_route, user)
 
       expect(response).to have_http_status(200)
       expect(response).to include_pagination_headers