diff --git a/docs/docs.go b/docs/docs.go
index aad76131fffdf4511f3699ffc3dafc59aa1bc6a5..cb0a9f15cd85048e3c31dbb8c21139959e23b3dd 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -33,6 +33,35 @@ const docTemplate = `{
                         }
                     }
                 }
+            },
+            "put": {
+                "description": "Add new course",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "course"
+                ],
+                "summary": "Add new course",
+                "parameters": [
+                    {
+                        "description": "Payload",
+                        "name": "data",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/add.AddCourseRequestPayload"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/web.BaseResponse"
+                        }
+                    }
+                }
             }
         },
         "/admin/user": {
@@ -551,6 +580,51 @@ const docTemplate = `{
         }
     },
     "definitions": {
+        "add.AddCourseRequestPayload": {
+            "description": "Information that should be available when you add a course",
+            "type": "object",
+            "required": [
+                "abbreviation",
+                "email",
+                "id",
+                "name"
+            ],
+            "properties": {
+                "abbreviation": {
+                    "description": "Course Name Abbreviation",
+                    "type": "string"
+                },
+                "addCourseToken": {
+                    "description": "Web Token that was appended to the link",
+                    "type": "string"
+                },
+                "description": {
+                    "description": "Course Description (Can be left empty)",
+                    "type": "string"
+                },
+                "email": {
+                    "description": "Contributor Email",
+                    "type": "string",
+                    "example": "someone@example.com"
+                },
+                "id": {
+                    "description": "Course ID",
+                    "type": "string"
+                },
+                "majabbr": {
+                    "description": "Course Major Abbreviation",
+                    "type": "string"
+                },
+                "major_id": {
+                    "description": "Major Id, will be set by the server",
+                    "type": "string"
+                },
+                "name": {
+                    "description": "Course Name",
+                    "type": "string"
+                }
+            }
+        },
         "admin.AdminAddUserPayload": {
             "type": "object",
             "required": [
diff --git a/docs/swagger.json b/docs/swagger.json
index 5e40a0e52c5c837cf1a9c167f1e4ab064fce87f3..f89ae6961a309970cc486d8abe9fdcab656521dd 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -25,6 +25,35 @@
                         }
                     }
                 }
+            },
+            "put": {
+                "description": "Add new course",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "course"
+                ],
+                "summary": "Add new course",
+                "parameters": [
+                    {
+                        "description": "Payload",
+                        "name": "data",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/add.AddCourseRequestPayload"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/web.BaseResponse"
+                        }
+                    }
+                }
             }
         },
         "/admin/user": {
@@ -543,6 +572,51 @@
         }
     },
     "definitions": {
+        "add.AddCourseRequestPayload": {
+            "description": "Information that should be available when you add a course",
+            "type": "object",
+            "required": [
+                "abbreviation",
+                "email",
+                "id",
+                "name"
+            ],
+            "properties": {
+                "abbreviation": {
+                    "description": "Course Name Abbreviation",
+                    "type": "string"
+                },
+                "addCourseToken": {
+                    "description": "Web Token that was appended to the link",
+                    "type": "string"
+                },
+                "description": {
+                    "description": "Course Description (Can be left empty)",
+                    "type": "string"
+                },
+                "email": {
+                    "description": "Contributor Email",
+                    "type": "string",
+                    "example": "someone@example.com"
+                },
+                "id": {
+                    "description": "Course ID",
+                    "type": "string"
+                },
+                "majabbr": {
+                    "description": "Course Major Abbreviation",
+                    "type": "string"
+                },
+                "major_id": {
+                    "description": "Major Id, will be set by the server",
+                    "type": "string"
+                },
+                "name": {
+                    "description": "Course Name",
+                    "type": "string"
+                }
+            }
+        },
         "admin.AdminAddUserPayload": {
             "type": "object",
             "required": [
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index d2280c2b16106e29102e0ed33e5e14d8ac794bc6..72093ff9594e71e7ccb9005fbf5c1d2c8f52474a 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -1,4 +1,38 @@
 definitions:
+  add.AddCourseRequestPayload:
+    description: Information that should be available when you add a course
+    properties:
+      abbreviation:
+        description: Course Name Abbreviation
+        type: string
+      addCourseToken:
+        description: Web Token that was appended to the link
+        type: string
+      description:
+        description: Course Description (Can be left empty)
+        type: string
+      email:
+        description: Contributor Email
+        example: someone@example.com
+        type: string
+      id:
+        description: Course ID
+        type: string
+      majabbr:
+        description: Course Major Abbreviation
+        type: string
+      major_id:
+        description: Major Id, will be set by the server
+        type: string
+      name:
+        description: Course Name
+        type: string
+    required:
+    - abbreviation
+    - email
+    - id
+    - name
+    type: object
   admin.AdminAddUserPayload:
     properties:
       email:
@@ -171,6 +205,25 @@ paths:
       summary: Index page
       tags:
       - common
+    put:
+      description: Add new course
+      parameters:
+      - description: Payload
+        in: body
+        name: data
+        required: true
+        schema:
+          $ref: '#/definitions/add.AddCourseRequestPayload'
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          schema:
+            $ref: '#/definitions/web.BaseResponse'
+      summary: Add new course
+      tags:
+      - course
   /admin/user:
     get:
       description: Get all users from database
diff --git a/handler/course/addCourse.go b/handler/course/addCourse.go
index fd02a41075794b23625bc3a106a9914eb48372f7..ebe3d703a68bc143f720be2dbb8d6cd22405c9f1 100644
--- a/handler/course/addCourse.go
+++ b/handler/course/addCourse.go
@@ -21,7 +21,6 @@ import (
 //	@Router				/ [put]
 func (c CourseHandlerImpl) AddCourse(w http.ResponseWriter, r *http.Request) {
 	payload := add.AddCourseRequestPayload{}
-	validate := validator.New()
 
 	// Validate payload
 	if r.Header.Get("Content-Type") != "application/json" {
@@ -36,6 +35,7 @@ func (c CourseHandlerImpl) AddCourse(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	validate := validator.New()
 	if err := validate.Struct(payload); err != nil {
 		if _, ok := err.(*validator.InvalidValidationError); ok {
 			payload := c.WrapperUtil.ErrorResponseWrap(err.Error(), nil)
diff --git a/handler/di.go b/handler/di.go
index 0f3eae80a5bd38faa6a1e07e63e13a9c6c41e11e..643a1fcfdc45a1bb37cd1e2e197932b5e320cb55 100644
--- a/handler/di.go
+++ b/handler/di.go
@@ -6,6 +6,7 @@ import (
 	"gitlab.informatika.org/ocw/ocw-backend/handler/auth"
 	"gitlab.informatika.org/ocw/ocw-backend/handler/common"
 	"gitlab.informatika.org/ocw/ocw-backend/handler/course"
+	"gitlab.informatika.org/ocw/ocw-backend/handler/material"
 	"gitlab.informatika.org/ocw/ocw-backend/handler/reset"
 	"gitlab.informatika.org/ocw/ocw-backend/handler/swagger"
 )
@@ -18,11 +19,11 @@ var HandlerSet = wire.NewSet(
 	// Swagger
 	wire.Struct(new(swagger.SwaggerHandlerImpl), "*"),
 	wire.Bind(new(swagger.SwaggerHandler), new(*swagger.SwaggerHandlerImpl)),
-	
+
 	// Admin
 	wire.Struct(new(admin.AdminHandlerImpl), "*"),
 	wire.Bind(new(admin.AdminHandler), new(*admin.AdminHandlerImpl)),
-	
+
 	// Auth
 	wire.Struct(new(auth.AuthHandlerImpl), "*"),
 	wire.Bind(new(auth.AuthHandler), new(*auth.AuthHandlerImpl)),
@@ -34,4 +35,8 @@ var HandlerSet = wire.NewSet(
 	// Course
 	wire.Struct(new(course.CourseHandlerImpl), "*"),
 	wire.Bind(new(course.CourseHandler), new(*course.CourseHandlerImpl)),
+
+	// Material
+	wire.Struct(new(material.MaterialHandlerImpl), "*"),
+	wire.Bind(new(material.MaterialHandler), new(*material.MaterialHandlerImpl)),
 )
diff --git a/handler/material/add_content.go b/handler/material/add_content.go
new file mode 100644
index 0000000000000000000000000000000000000000..3a6823fa495d825d95be1b612c8647c20fa94dc7
--- /dev/null
+++ b/handler/material/add_content.go
@@ -0,0 +1,99 @@
+package material
+
+import (
+	"net/http"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/go-playground/validator/v10"
+	"github.com/google/uuid"
+	"gitlab.informatika.org/ocw/ocw-backend/middleware/guard"
+	materialDomain "gitlab.informatika.org/ocw/ocw-backend/model/domain/material"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web"
+	authToken "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/material"
+)
+
+func (m MaterialHandlerImpl) AddContent(w http.ResponseWriter, r *http.Request) {
+	payload := material.NewContentRequest{}
+	user, ok := r.Context().Value(guard.UserContext).(authToken.UserClaim)
+
+	materialId := chi.URLParam(r, "material-id")
+	if materialId == "" {
+		payload := m.WrapperUtil.ErrorResponseWrap("material id is required", nil)
+		m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+	}
+
+	materialIdUUID, err := uuid.Parse(materialId)
+	if err != nil {
+		payload := m.WrapperUtil.ErrorResponseWrap("material id is invalid", err)
+		m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+	}
+
+	payload.MaterialId = materialIdUUID
+
+	if !ok {
+		payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+		m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		return
+	}
+
+	validate := validator.New()
+
+	if r.Header.Get("Content-Type") != "application/json" {
+		payload := m.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil)
+		m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload)
+		return
+	}
+
+	if err := m.HttpUtil.ParseJson(r, &payload); err != nil {
+		payload := m.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error())
+		m.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload)
+		return
+	}
+
+	if err := validate.Struct(payload); err != nil {
+		if _, ok := err.(*validator.InvalidValidationError); ok {
+			payload := m.WrapperUtil.ErrorResponseWrap(err.Error(), nil)
+			m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+			return
+		}
+
+		errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors))
+		payload := m.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload)
+		m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	if payload.Type != materialDomain.Handout && payload.Link == "" {
+		payload := m.WrapperUtil.ErrorResponseWrap("link is required", nil)
+		m.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload)
+		return
+	}
+
+	uploadLink, err := m.MaterialContentService.AddContent(payload.MaterialId, user.Email, materialDomain.Content{
+		Type: payload.Type,
+		Link: payload.Link,
+	})
+
+	if err != nil {
+		respErr, ok := err.(web.ResponseError)
+		if ok {
+			payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr)
+
+			if respErr.Code != "NOT_OWNER" {
+				m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+			} else {
+				m.HttpUtil.WriteJson(w, http.StatusForbidden, payload)
+			}
+		} else {
+			payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+			m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		}
+		return
+	}
+
+	responsePayload := m.WrapperUtil.SuccessResponseWrap(material.NewContentResponse{
+		UploadLink: uploadLink,
+	})
+	m.HttpUtil.WriteSuccessJson(w, responsePayload)
+}
diff --git a/handler/material/create_material.go b/handler/material/create_material.go
new file mode 100644
index 0000000000000000000000000000000000000000..de44a1baadaab8130098fc59a8fa88c9e5c44846
--- /dev/null
+++ b/handler/material/create_material.go
@@ -0,0 +1,94 @@
+package material
+
+import (
+	"net/http"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/go-playground/validator/v10"
+	"gitlab.informatika.org/ocw/ocw-backend/middleware/guard"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web"
+	authToken "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web/material"
+)
+
+func (m MaterialHandlerImpl) CreateMaterial(w http.ResponseWriter, r *http.Request) {
+	payload := material.CreateMaterialRequest{}
+	courseId := chi.URLParam(r, "id")
+
+	// START OF VALIDATE
+	if r.Header.Get("Content-Type") != "application/json" {
+		payload := m.WrapperUtil.ErrorResponseWrap("this service only receive json input", nil)
+		m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload)
+		return
+	}
+
+	if err := m.HttpUtil.ParseJson(r, &payload); err != nil {
+		payload := m.WrapperUtil.ErrorResponseWrap("invalid json input", err.Error())
+		m.HttpUtil.WriteJson(w, http.StatusUnprocessableEntity, payload)
+		return
+	}
+
+	validate := validator.New()
+	if err := validate.Struct(payload); err != nil {
+		if _, ok := err.(*validator.InvalidValidationError); ok {
+			payload := m.WrapperUtil.ErrorResponseWrap(err.Error(), nil)
+			m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+			return
+		}
+
+		errPayload := web.NewResponseErrorFromValidator(err.(validator.ValidationErrors))
+		payload := m.WrapperUtil.ErrorResponseWrap(errPayload.Error(), errPayload)
+		m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+	// END OF VALIDATE
+
+	if courseId == "" {
+		payload := m.WrapperUtil.ErrorResponseWrap("course id is required", nil)
+		m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	if isExist, _ := m.CourseRepository.IsCourseExist(courseId); !isExist {
+		payload := m.WrapperUtil.ErrorResponseWrap("course id not found", nil)
+		m.HttpUtil.WriteJson(w, http.StatusNotFound, payload)
+		return
+	}
+
+	user, ok := r.Context().Value(guard.UserContext).(authToken.UserClaim)
+
+	if !ok {
+		m.Logger.Error("Context is not found")
+		payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+		m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		return
+	}
+
+	id, err := m.MaterialService.Create(
+		courseId,
+		user.Email,
+		payload.Name,
+	)
+
+	if err != nil {
+		respErr, ok := err.(web.ResponseError)
+		if ok {
+			payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr)
+
+			if respErr.Code != "NOT_OWNER" {
+				m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+			} else {
+				m.HttpUtil.WriteJson(w, http.StatusForbidden, payload)
+			}
+		} else {
+			payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+			m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		}
+		return
+	}
+
+	responsePayload := m.WrapperUtil.SuccessResponseWrap(material.CreateMaterialResponse{
+		MaterialId: id,
+	})
+	m.HttpUtil.WriteSuccessJson(w, responsePayload)
+}
diff --git a/handler/material/delete_content.go b/handler/material/delete_content.go
new file mode 100644
index 0000000000000000000000000000000000000000..8b5cfe0a3cbaf79f7eb209fa8cbaccb4b2a58608
--- /dev/null
+++ b/handler/material/delete_content.go
@@ -0,0 +1,77 @@
+package material
+
+import (
+	"net/http"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/google/uuid"
+	"gitlab.informatika.org/ocw/ocw-backend/middleware/guard"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web"
+	authToken "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token"
+)
+
+func (m MaterialHandlerImpl) DeleteContent(w http.ResponseWriter, r *http.Request) {
+	materialIdUnparsed := chi.URLParam(r, "material-id")
+
+	if materialIdUnparsed == "" {
+		payload := m.WrapperUtil.ErrorResponseWrap("material id is required", nil)
+		m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		return
+	}
+
+	materialId, err := uuid.Parse(materialIdUnparsed)
+	if err != nil {
+		payload := m.WrapperUtil.ErrorResponseWrap("material id is invalid", nil)
+		m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	contentIdUnparsed := chi.URLParam(r, "content-id")
+
+	if contentIdUnparsed == "" {
+		payload := m.WrapperUtil.ErrorResponseWrap("content id is required", nil)
+		m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		return
+	}
+
+	contentId, err := uuid.Parse(contentIdUnparsed)
+	if err != nil {
+		payload := m.WrapperUtil.ErrorResponseWrap("material id is invalid", nil)
+		m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	user, ok := r.Context().Value(guard.UserContext).(authToken.UserClaim)
+
+	if !ok {
+		payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+		m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		return
+	}
+
+	err = m.MaterialContentService.DeleteContent(
+		materialId,
+		user.Email,
+		contentId,
+	)
+
+	if err != nil {
+		respErr, ok := err.(web.ResponseError)
+		if ok {
+			payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr)
+
+			if respErr.Code != "NOT_OWNER" {
+				m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+			} else {
+				m.HttpUtil.WriteJson(w, http.StatusForbidden, payload)
+			}
+		} else {
+			payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+			m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		}
+		return
+	}
+
+	responsePayload := m.WrapperUtil.SuccessResponseWrap(nil)
+	m.HttpUtil.WriteSuccessJson(w, responsePayload)
+}
diff --git a/handler/material/delete_material.go b/handler/material/delete_material.go
new file mode 100644
index 0000000000000000000000000000000000000000..6451f0709eddf11bb429f64576ced94c67ef1c39
--- /dev/null
+++ b/handler/material/delete_material.go
@@ -0,0 +1,62 @@
+package material
+
+import (
+	"net/http"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/google/uuid"
+	"gitlab.informatika.org/ocw/ocw-backend/middleware/guard"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web"
+	authToken "gitlab.informatika.org/ocw/ocw-backend/model/web/auth/token"
+)
+
+func (m MaterialHandlerImpl) DeleteMaterial(w http.ResponseWriter, r *http.Request) {
+	materialIdUnparsed := chi.URLParam(r, "material-id")
+
+	if materialIdUnparsed == "" {
+		payload := m.WrapperUtil.ErrorResponseWrap("material id is required", nil)
+		m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		return
+	}
+
+	materialId, err := uuid.Parse(materialIdUnparsed)
+	if err != nil {
+		payload := m.WrapperUtil.ErrorResponseWrap("material id is invalid", nil)
+		m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+		return
+	}
+
+	user, ok := r.Context().Value(guard.UserContext).(authToken.UserClaim)
+
+	if !ok {
+		payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+		m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		return
+	}
+
+	err = m.MaterialService.Delete(
+		materialId,
+		user.Email,
+	)
+
+	if err != nil {
+		respErr, ok := err.(web.ResponseError)
+		if ok {
+			payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr)
+
+			if respErr.Code != "NOT_OWNER" {
+				m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+			} else {
+				m.HttpUtil.WriteJson(w, http.StatusForbidden, payload)
+			}
+		} else {
+			payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+			m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		}
+		return
+	}
+
+	responsePayload := m.WrapperUtil.SuccessResponseWrap(nil)
+	m.HttpUtil.WriteSuccessJson(w, responsePayload)
+
+}
diff --git a/handler/material/get_material.go b/handler/material/get_material.go
new file mode 100644
index 0000000000000000000000000000000000000000..f39195f784281d6dfa53f40535f6783b762c8699
--- /dev/null
+++ b/handler/material/get_material.go
@@ -0,0 +1,40 @@
+package material
+
+import (
+	"net/http"
+
+	"github.com/go-chi/chi/v5"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web"
+)
+
+func (m MaterialHandlerImpl) GetMaterial(w http.ResponseWriter, r *http.Request) {
+	courseId := chi.URLParam(r, "id")
+
+	if courseId == "" {
+		payload := m.WrapperUtil.ErrorResponseWrap("course id is required", nil)
+		m.HttpUtil.WriteJson(w, http.StatusUnsupportedMediaType, payload)
+		return
+	}
+
+	result, err := m.MaterialService.Get(courseId)
+
+	if err != nil {
+		respErr, ok := err.(web.ResponseError)
+		if ok {
+			payload := m.WrapperUtil.ErrorResponseWrap(respErr.Error(), respErr)
+
+			if respErr.Code != "NOT_OWNER" {
+				m.HttpUtil.WriteJson(w, http.StatusBadRequest, payload)
+			} else {
+				m.HttpUtil.WriteJson(w, http.StatusForbidden, payload)
+			}
+		} else {
+			payload := m.WrapperUtil.ErrorResponseWrap("internal server error", nil)
+			m.HttpUtil.WriteJson(w, http.StatusInternalServerError, payload)
+		}
+		return
+	}
+
+	responsePayload := m.WrapperUtil.SuccessResponseWrap(result)
+	m.HttpUtil.WriteSuccessJson(w, responsePayload)
+}
diff --git a/handler/material/impl.go b/handler/material/impl.go
new file mode 100644
index 0000000000000000000000000000000000000000..647ce941833b3c696b66271c41dd2565bb23a874
--- /dev/null
+++ b/handler/material/impl.go
@@ -0,0 +1,18 @@
+package material
+
+import (
+	"gitlab.informatika.org/ocw/ocw-backend/repository/course"
+	"gitlab.informatika.org/ocw/ocw-backend/service/logger"
+	"gitlab.informatika.org/ocw/ocw-backend/service/material"
+	"gitlab.informatika.org/ocw/ocw-backend/utils/httputil"
+	"gitlab.informatika.org/ocw/ocw-backend/utils/wrapper"
+)
+
+type MaterialHandlerImpl struct {
+	material.MaterialService
+	material.MaterialContentService
+	httputil.HttpUtil
+	wrapper.WrapperUtil
+	course.CourseRepository
+	logger.Logger
+}
diff --git a/handler/material/types.go b/handler/material/types.go
new file mode 100644
index 0000000000000000000000000000000000000000..1c7cb4534dbb5780a6e3fbe5bd38f31d7bbb6369
--- /dev/null
+++ b/handler/material/types.go
@@ -0,0 +1,12 @@
+package material
+
+import "net/http"
+
+type MaterialHandler interface {
+	AddContent(w http.ResponseWriter, r *http.Request)
+	DeleteContent(w http.ResponseWriter, r *http.Request)
+
+	CreateMaterial(w http.ResponseWriter, r *http.Request)
+	DeleteMaterial(w http.ResponseWriter, r *http.Request)
+	GetMaterial(w http.ResponseWriter, r *http.Request)
+}
diff --git a/middleware/guard/guard.go b/middleware/guard/guard.go
index 3b71984a870188559f7345793d786ccfce0f1e6d..487b5cf8e050ef23a52ae79f4afdbab5ca050871 100644
--- a/middleware/guard/guard.go
+++ b/middleware/guard/guard.go
@@ -83,7 +83,7 @@ func (g GuardMiddleware) Handle(next http.Handler) http.Handler {
 				return
 			}
 
-			ctx := context.WithValue(r.Context(), UserContext, claim)
+			ctx := context.WithValue(r.Context(), UserContext, *claim)
 			next.ServeHTTP(w, r.WithContext(ctx))
 			return
 		}
diff --git a/model/domain/material/content.go b/model/domain/material/content.go
index e65e43733da4a1da2c2c71a68780f56cc8f97a72..c68f1587cd6dd653f04433cca81a1946d1ad0277 100644
--- a/model/domain/material/content.go
+++ b/model/domain/material/content.go
@@ -3,10 +3,10 @@ package material
 import "github.com/google/uuid"
 
 type Content struct {
-	Id         uuid.UUID    `json:"id" gorm:"primaryKey"`
+	Id         uuid.UUID    `gorm:"primaryKey" json:"id"`
 	Type       MaterialType `json:"type"`
 	Link       string       `json:"link"`
-	MaterialId uuid.UUID    `json:"material_id"`
+	MaterialID uuid.UUID    `json:"material_id"`
 }
 
 func (Content) TableName() string {
diff --git a/model/domain/material/material.go b/model/domain/material/material.go
index e2fce4d5d6a6cd31d802468fb65eb8bc5f3d54fc..f3b11b7f8820cd71ecf03415eb9339b9a739d2fb 100644
--- a/model/domain/material/material.go
+++ b/model/domain/material/material.go
@@ -2,17 +2,14 @@ package material
 
 import (
 	"github.com/google/uuid"
-	"gitlab.informatika.org/ocw/ocw-backend/model/domain/course"
-	"gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
 )
 
 type Material struct {
-	Id           uuid.UUID      `json:"id" gorm:"primaryKey"`
-	CourseId     string         `json:"course_id"`
-	CreatorEmail string         `json:"creator_email"`
-	Creator      *user.User     `json:"creator" gorm:"foreignKey:CreatorEmail;references:Email"`
-	Course       *course.Course `json:"course" gorm:"foreignKey:CourseId;references:Id"`
-	Contents     []Content      `json:"contents" gorm:"foreignKey:MaterialId;references:Id"`
+	ID           uuid.UUID `json:"id" gorm:"primaryKey"`
+	CourseId     string    `json:"course_id"`
+	CreatorEmail string    `json:"creator_email"`
+	Name         string    `json:"name"`
+	Contents     []Content `json:"contents"`
 }
 
 func (Material) TableName() string {
diff --git a/model/domain/material/material_type.go b/model/domain/material/material_type.go
index 10e8abdc730e658f28f99da007bb755fc6896795..b8c7aec0193c2c71384f36b5ce2731d24be5f850 100644
--- a/model/domain/material/material_type.go
+++ b/model/domain/material/material_type.go
@@ -10,8 +10,8 @@ import (
 type MaterialType int
 
 const (
-	Video MaterialType = iota
-	Handout
+	Handout MaterialType = iota + 1
+	Video
 	External
 )
 
diff --git a/model/domain/quiz/options.go b/model/domain/quiz/options.go
index de062c3cc531f34b4572016248e2085a2d1f1d99..d2bdddbf7da440ce8b48385cd7a5d5b0f118bcef 100644
--- a/model/domain/quiz/options.go
+++ b/model/domain/quiz/options.go
@@ -3,10 +3,10 @@ package quiz
 import "github.com/google/uuid"
 
 type AnswerOption struct {
-	Id            uuid.UUID `gorm:"primaryKey"`
-	QuizProblemId uuid.UUID `gorm:"primaryKey"`
-	Statement     string
-	IsAnswer      bool
+	Id            uuid.UUID `gorm:"primaryKey" json:"id"`
+	QuizProblemId uuid.UUID `gorm:"primaryKey" json:"problem_id"`
+	Statement     string    `json:"statement"`
+	IsAnswer      bool      `json:"isAnswer"`
 }
 
 func (AnswerOption) TableName() string {
diff --git a/model/domain/quiz/quiz.go b/model/domain/quiz/quiz.go
index 6caade417ae857ec2a78fdf8c13b5ef007a8529a..f4dcce5a8155d1c0a670dd962bd549233d95b2d8 100644
--- a/model/domain/quiz/quiz.go
+++ b/model/domain/quiz/quiz.go
@@ -7,13 +7,13 @@ import (
 )
 
 type Quiz struct {
-	Id           uuid.UUID `gorm:"primaryKey"`
-	Name         string
-	CourseId     string
-	CreatorEmail string
-	Creator      user.User     `gorm:"foreignKey:CreatorEmail;references:Email"`
-	Course       course.Course `gorm:"foreignKey:CourseId;references:Id"`
-	Problems     []QuizProblem `gorm:"foreignKey:QuizId;references:Id"`
+	Id           uuid.UUID     `gorm:"primaryKey" json:"id"`
+	Name         string        `json:"name"`
+	CourseId     string        `json:"course_id"`
+	CreatorEmail string        `json:"creator_email"`
+	Creator      user.User     `gorm:"foreignKey:CreatorEmail;references:Email" json:"creator"`
+	Course       course.Course `gorm:"foreignKey:CourseId;references:Id" json:"course"`
+	Problems     []QuizProblem `gorm:"foreignKey:QuizId;references:Id" json:"problems"`
 }
 
 func (Quiz) TableName() string {
diff --git a/model/domain/quiz/quiz_problem.go b/model/domain/quiz/quiz_problem.go
index 3ce4195465d2fd477567f18858296d949c60020c..1bcdcf880ad9eaeeadd164cf60d253fd9eedb8db 100644
--- a/model/domain/quiz/quiz_problem.go
+++ b/model/domain/quiz/quiz_problem.go
@@ -3,11 +3,11 @@ package quiz
 import "github.com/google/uuid"
 
 type QuizProblem struct {
-	Id        uuid.UUID `gorm:"primaryKey"`
-	Statement string
-	Type      ProblemType
-	QuizId    uuid.UUID
-	Options   []AnswerOption `gorm:"foreignKey:QuizProblemId;references:Id"`
+	Id        uuid.UUID      `gorm:"primaryKey" json:"id"`
+	Statement string         `json:"statement"`
+	Type      ProblemType    `json:"type"`
+	QuizId    uuid.UUID      `json:"quiz_id"`
+	Options   []AnswerOption `gorm:"foreignKey:QuizProblemId;references:Id" json:"options"`
 }
 
 func (QuizProblem) TableName() string {
diff --git a/model/domain/quiz/take.go b/model/domain/quiz/take.go
index c66263f60f1e4009019e5dcd428e7c905784f243..2c707ab601da28e630420015393607aef0f9d228 100644
--- a/model/domain/quiz/take.go
+++ b/model/domain/quiz/take.go
@@ -8,15 +8,15 @@ import (
 )
 
 type QuizTake struct {
-	Id            uuid.UUID `gorm:"primaryKey"`
-	QuizId        uuid.UUID
-	Email         string
-	StartTime     time.Time
-	IsFinished    bool
-	Score         int
-	Quiz          `gorm:"foreignKey:QuizId;references:Id"`
-	user.User     `gorm:"foreignKey:Email;references:Email"`
-	ChoiceAnswers []TakeChoiceAnswer `gorm:"foreignKey:QuizTakeId;references:Id"`
+	Id            uuid.UUID `gorm:"primaryKey" json:"id"`
+	QuizId        uuid.UUID `json:"quiz_id"`
+	Email         string    `json:"email"`
+	StartTime     time.Time `json:"start"`
+	IsFinished    bool      `json:"finished"`
+	Score         int       `json:"score"`
+	Quiz          `gorm:"foreignKey:QuizId;references:Id" json:"quiz"`
+	user.User     `gorm:"foreignKey:Email;references:Email" json:"user"`
+	ChoiceAnswers []TakeChoiceAnswer `gorm:"foreignKey:QuizTakeId;references:Id" json:"-"`
 }
 
 func (QuizTake) TableName() string {
diff --git a/model/domain/user/user.go b/model/domain/user/user.go
index f2fbb565f6dd4aa94cbf7f15b260980ead0351f9..ad56084aa4fddda0f60e71d8de8819589e467bf1 100644
--- a/model/domain/user/user.go
+++ b/model/domain/user/user.go
@@ -3,13 +3,13 @@ package user
 import "time"
 
 type User struct {
-	Email       string `gorm:"primaryKey"`
-	Password    string
-	Name        string
-	Role        UserRole `gorm:"type:user_role"`
-	IsActivated bool
-	CreatedAt   time.Time
-	UpdatedAt   time.Time
+	Email       string    `gorm:"primaryKey" json:"email"`
+	Password    string    `json:"-"`
+	Name        string    `json:"name"`
+	Role        UserRole  `gorm:"type:user_role" json:"role"`
+	IsActivated bool      `json:"activated"`
+	CreatedAt   time.Time `json:"created_at"`
+	UpdatedAt   time.Time `json:"updated_at"`
 }
 
 func (User) TableName() string {
diff --git a/model/web/material/content.go b/model/web/material/content.go
new file mode 100644
index 0000000000000000000000000000000000000000..d3575b4a9314057b0816d2316bade8e250f610aa
--- /dev/null
+++ b/model/web/material/content.go
@@ -0,0 +1,21 @@
+package material
+
+import (
+	"github.com/google/uuid"
+	"gitlab.informatika.org/ocw/ocw-backend/model/domain/material"
+)
+
+type NewContentRequest struct {
+	Type       material.MaterialType `json:"type" validate:"required"`
+	Link       string                `json:"link"`
+	MaterialId uuid.UUID             `json:"-"`
+}
+
+type NewContentResponse struct {
+	UploadLink string `json:"upload_link"`
+}
+
+type DeleteContentRequest struct {
+	ContentId  uuid.UUID `json:"content_id"`
+	MaterialId uuid.UUID `json:"-"`
+}
diff --git a/model/web/material/material.go b/model/web/material/material.go
new file mode 100644
index 0000000000000000000000000000000000000000..98f8c6cdae84025cf0079c2df683a3be1767420b
--- /dev/null
+++ b/model/web/material/material.go
@@ -0,0 +1,15 @@
+package material
+
+import "github.com/google/uuid"
+
+type CreateMaterialRequest struct {
+	Name string `json:"name" validate:"required"`
+}
+
+type CreateMaterialResponse struct {
+	MaterialId uuid.UUID `json:"material_id"`
+}
+
+type DeleteMaterialRequest struct {
+	MaterialId uuid.UUID `json:"material_id"`
+}
diff --git a/provider/storage/s3.go b/provider/storage/s3.go
index 3f746712f3fa34c1ae77ea87fe4491282e4a6609..52aaaaba702a12c947c98df8ecef737db8ed3009 100644
--- a/provider/storage/s3.go
+++ b/provider/storage/s3.go
@@ -14,10 +14,15 @@ type S3Storage struct {
 func NewS3(
 	env *env.Environment,
 ) (*S3Storage, error) {
-	client, err := minio.New(env.BucketEndpoint, &minio.Options{
+	if !env.UseBucket {
+		return nil, nil
+	}
+
+	settings := &minio.Options{
 		Creds:  credentials.NewStaticV4(env.BucketAccessKey, env.BucketSecretKey, env.BucketTokenKey),
 		Secure: env.BucketUseSSL,
-	})
+	}
+	client, err := minio.New(env.BucketEndpoint, settings)
 
 	if err != nil {
 		return nil, err
diff --git a/repository/course/course.go b/repository/course/course.go
index cc092bd007f4a0346c668db6425bc2acb5f19db5..1dbeee3dd4f0ed06fbb3daf9655e10fc67a8c579 100644
--- a/repository/course/course.go
+++ b/repository/course/course.go
@@ -33,6 +33,20 @@ func (repo CourseRepositoryImpl) IsCourseExist(id string) (bool, error) {
 	return true, nil
 }
 
+func (repo CourseRepositoryImpl) IsUserCourseContributor(id string, email string) (bool, error) {
+	err := repo.db.Where("course_id = ? AND email = ?").Find(&course.Course{}).Error
+
+	if err == nil {
+		return true, nil
+	}
+
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+		return false, nil
+	}
+
+	return false, err
+}
+
 func (repo CourseRepositoryImpl) IsMajorExist(id uuid.UUID) (bool, error) {
 	_, err := repo.GetMajor(id)
 
diff --git a/repository/course/type.go b/repository/course/type.go
index 252eb470be01f16794cbbb37dc3f85fee1ecfca0..7346d0a0551bed19e126fb1922a8cb4cacacdc1d 100644
--- a/repository/course/type.go
+++ b/repository/course/type.go
@@ -25,9 +25,10 @@ type CourseRepository interface {
 	IsCourseExist(id string) (bool, error)
 	IsMajorExist(id uuid.UUID) (bool, error)
 	IsFacultyExist(id uuid.UUID) (bool, error)
+	IsUserCourseContributor(id string, email string) (bool, error)
 
 	// Internal Method Only
-	
+
 	GetMajorByAbbr(abbr string) (*course.Major, error)
 	GetFacultyByAbbr(abbr string) (*course.Faculty, error)
 }
diff --git a/repository/material/content.go b/repository/material/content.go
index 0f321a1b58f824fecb3bfeb5b5ea5b26c331b434..919d17b8fa531fe88c867194c154b52a3bd710ae 100644
--- a/repository/material/content.go
+++ b/repository/material/content.go
@@ -3,17 +3,35 @@ package material
 import (
 	"github.com/google/uuid"
 	"gitlab.informatika.org/ocw/ocw-backend/model/domain/material"
+	"gitlab.informatika.org/ocw/ocw-backend/provider/db"
+	"gitlab.informatika.org/ocw/ocw-backend/repository/course"
 	"gitlab.informatika.org/ocw/ocw-backend/repository/transaction"
+	"gorm.io/gorm"
 )
 
 type MaterialContentRepositoryImpl struct {
 	builder transaction.TransactionBuilder
+	course.CourseRepository
+	MaterialRepository
+	db *gorm.DB
 }
 
 func NewMaterialContent(
 	builder transaction.TransactionBuilder,
+	course course.CourseRepository,
+	database db.Database,
+	material MaterialRepository,
 ) *MaterialContentRepositoryImpl {
-	return &MaterialContentRepositoryImpl{builder}
+	return &MaterialContentRepositoryImpl{builder, course, material, database.Connect()}
+}
+
+func (m MaterialContentRepositoryImpl) IsUserContributor(id uuid.UUID, email string) (bool, error) {
+	result := &material.Content{}
+	if err := m.db.Where("id = ?", id).Find(result).Error; err != nil {
+		return false, err
+	}
+
+	return m.MaterialRepository.IsUserContributor(result.MaterialID, email)
 }
 
 func (m MaterialContentRepositoryImpl) New(
@@ -35,7 +53,8 @@ func (m MaterialContentRepositoryImpl) NewWithTransaction(
 	materialType material.MaterialType,
 	link string) (uuid.UUID, error) {
 	contentData := material.Content{
-		MaterialId: materialId,
+		Id:         uuid.New(),
+		MaterialID: materialId,
 		Type:       materialType,
 		Link:       link,
 	}
diff --git a/repository/material/material.go b/repository/material/material.go
index a20281eab9223275befc37993d0a0a3383e9dc86..9cecd65f3fec546c254cfd731fde21660b4f684e 100644
--- a/repository/material/material.go
+++ b/repository/material/material.go
@@ -3,27 +3,42 @@ package material
 import (
 	"github.com/google/uuid"
 	"gitlab.informatika.org/ocw/ocw-backend/model/domain/material"
+	"gitlab.informatika.org/ocw/ocw-backend/provider/db"
 	"gitlab.informatika.org/ocw/ocw-backend/repository/transaction"
+	"gorm.io/gorm"
 )
 
 type MaterialRepositoryImpl struct {
 	builder transaction.TransactionBuilder
+	db      *gorm.DB
 }
 
 func NewMaterial(
 	builder transaction.TransactionBuilder,
+	db db.Database,
 ) *MaterialRepositoryImpl {
-	return &MaterialRepositoryImpl{builder}
+	return &MaterialRepositoryImpl{builder, db.Connect()}
 }
 
-func (m MaterialRepositoryImpl) New(courseId string, creatorEmail string) (uuid.UUID, error) {
-	return m.NewWithTransaction(m.builder.Build(), courseId, creatorEmail)
+func (m MaterialRepositoryImpl) IsUserContributor(id uuid.UUID, email string) (bool, error) {
+	err := m.db.Where("creator_email = ? AND id = ?", email, id).Find(&material.Material{}).Error
+	if err != nil {
+		return false, err
+	}
+
+	return true, nil
+}
+
+func (m MaterialRepositoryImpl) New(courseId string, creatorEmail string, name string) (uuid.UUID, error) {
+	return m.NewWithTransaction(m.builder.Build(), courseId, creatorEmail, name)
 }
 
-func (m MaterialRepositoryImpl) NewWithTransaction(tx transaction.Transaction, courseId string, creatorEmail string) (uuid.UUID, error) {
+func (m MaterialRepositoryImpl) NewWithTransaction(tx transaction.Transaction, courseId string, creatorEmail string, name string) (uuid.UUID, error) {
 	materialData := &material.Material{
+		ID:           uuid.New(),
 		CourseId:     courseId,
 		CreatorEmail: creatorEmail,
+		Name:         name,
 	}
 
 	err := tx.GetTransaction().Create(materialData).Error
@@ -32,7 +47,7 @@ func (m MaterialRepositoryImpl) NewWithTransaction(tx transaction.Transaction, c
 		return uuid.Nil, err
 	}
 
-	return materialData.Id, nil
+	return materialData.ID, nil
 }
 
 func (m MaterialRepositoryImpl) Delete(id uuid.UUID) error {
@@ -49,7 +64,12 @@ func (m MaterialRepositoryImpl) GetAll(courseId string) ([]material.Material, er
 
 func (m MaterialRepositoryImpl) GetAllWithTransaction(tx transaction.Transaction, courseId string) ([]material.Material, error) {
 	result := []material.Material{}
-	err := tx.GetTransaction().Joins("Contents").Where("CourseId = ?", courseId).Find(&result).Error
+	trx := tx.GetTransaction()
+	err := trx.
+		Model(&material.Material{}).
+		Preload("Contents").
+		Where("course_id = ?", courseId).
+		Find(&result).Error
 
 	return result, err
 }
diff --git a/repository/material/type.go b/repository/material/type.go
index 8343c4df1757d7f4f585efd43fe3cc18e4f325d4..4a0d0920746e05e4ccdfead1096db39dda8402db 100644
--- a/repository/material/type.go
+++ b/repository/material/type.go
@@ -7,16 +7,19 @@ import (
 )
 
 type MaterialRepository interface {
-	New(courseId string, creatorEmail string) (uuid.UUID, error)
+	New(courseId string, creatorEmail string, name string) (uuid.UUID, error)
 	Delete(id uuid.UUID) error
 	GetAll(courseId string) ([]material.Material, error)
 
-	NewWithTransaction(tx transaction.Transaction, courseId string, creatorEmail string) (uuid.UUID, error)
+	IsUserContributor(id uuid.UUID, email string) (bool, error)
+
+	NewWithTransaction(tx transaction.Transaction, courseId string, creatorEmail string, name string) (uuid.UUID, error)
 	DeleteWithTransaction(tx transaction.Transaction, id uuid.UUID) error
 	GetAllWithTransaction(tx transaction.Transaction, courseId string) ([]material.Material, error)
 }
 
 type MaterialContentRepository interface {
+	IsUserContributor(id uuid.UUID, email string) (bool, error)
 	New(materialId uuid.UUID, materialType material.MaterialType, link string) (uuid.UUID, error)
 	GetAll(materialId uuid.UUID) ([]material.Content, error)
 	Delete(contentId uuid.UUID) error
diff --git a/routes/course/route.go b/routes/course/route.go
index 6fb5f6ac2019b24687bbc28d593011846bed1057..ccafadb198e645425c1374f6edf70265111a8cb5 100644
--- a/routes/course/route.go
+++ b/routes/course/route.go
@@ -3,10 +3,15 @@ package course
 import (
 	"github.com/go-chi/chi/v5"
 	"gitlab.informatika.org/ocw/ocw-backend/handler/course"
+	"gitlab.informatika.org/ocw/ocw-backend/handler/material"
+	"gitlab.informatika.org/ocw/ocw-backend/middleware/guard"
+	"gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
 )
 
 type CourseRoutes struct {
 	course.CourseHandler
+	material.MaterialHandler
+	*guard.GuardBuilder
 }
 
 func (c CourseRoutes) Register(r chi.Router) {
@@ -34,5 +39,11 @@ func (c CourseRoutes) Register(r chi.Router) {
 
 		// Delete
 		r.Delete("/{id}", c.CourseHandler.DeleteCourse)
+		r.Get("/{id}/materials", c.MaterialHandler.GetMaterial)
+	})
+
+	r.Route("/course/{id}/material", func(r chi.Router) {
+		r.Use(c.BuildSimple(user.Contributor))
+		r.Post("/", c.MaterialHandler.CreateMaterial)
 	})
 }
diff --git a/routes/di.go b/routes/di.go
index a51997ed13bc22c420ab390bcd1c46de58b20bce..e05148661337dd26d5400800ca389644f93aa3be 100644
--- a/routes/di.go
+++ b/routes/di.go
@@ -6,6 +6,7 @@ import (
 	"gitlab.informatika.org/ocw/ocw-backend/routes/auth"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/common"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/course"
+	"gitlab.informatika.org/ocw/ocw-backend/routes/material"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/reset"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/swagger"
 )
@@ -17,6 +18,7 @@ var routesCollectionSet = wire.NewSet(
 	wire.Struct(new(admin.AdminRoutes), "*"),
 	wire.Struct(new(reset.ResetRoutes), "*"),
 	wire.Struct(new(course.CourseRoutes), "*"),
+	wire.Struct(new(material.MaterialRoutes), "*"),
 )
 
 var RoutesSet = wire.NewSet(
diff --git a/routes/material/route.go b/routes/material/route.go
new file mode 100644
index 0000000000000000000000000000000000000000..31fc398e8a45b45537ac0c72b661c5f404fb239b
--- /dev/null
+++ b/routes/material/route.go
@@ -0,0 +1,26 @@
+package material
+
+import (
+	"github.com/go-chi/chi/v5"
+	"gitlab.informatika.org/ocw/ocw-backend/handler/material"
+	"gitlab.informatika.org/ocw/ocw-backend/middleware/guard"
+	"gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
+)
+
+type MaterialRoutes struct {
+	material.MaterialHandler
+	*guard.GuardBuilder
+}
+
+func (c MaterialRoutes) Register(r chi.Router) {
+	r.Route("/material/{material-id}", func(r chi.Router) {
+		r.Use(c.GuardBuilder.BuildSimple(user.Contributor))
+
+		// Add
+		r.Post("/content", c.AddContent)
+
+		// Delete
+		r.Delete("/", c.DeleteMaterial)
+		r.Delete("/content/{content-id}", c.DeleteContent)
+	})
+}
diff --git a/routes/routes.go b/routes/routes.go
index da4d5b4a4f8efd320ee9b8151f0b8ae4cf4a3757..63e1d79fb448d35384bc6f062d360cdb2a7a258f 100644
--- a/routes/routes.go
+++ b/routes/routes.go
@@ -5,6 +5,7 @@ import (
 	"gitlab.informatika.org/ocw/ocw-backend/routes/auth"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/common"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/course"
+	"gitlab.informatika.org/ocw/ocw-backend/routes/material"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/reset"
 	"gitlab.informatika.org/ocw/ocw-backend/routes/swagger"
 
@@ -19,6 +20,7 @@ type AppRouter struct {
 	auth.AuthRoutes
 	reset.ResetRoutes
 	course.CourseRoutes
+	material.MaterialRoutes
 
 	// Utility
 	Logger logger.Logger
diff --git a/service/material/content.go b/service/material/content.go
index 5bf0b5638d0e44d4857932fc3d782a6d0a36c45b..e54a1a0ac1eca2445939847036620231d885d801 100644
--- a/service/material/content.go
+++ b/service/material/content.go
@@ -1,47 +1,98 @@
 package material
 
 import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+
 	"github.com/google/uuid"
 	materialDomain "gitlab.informatika.org/ocw/ocw-backend/model/domain/material"
-	"gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web"
+	"gitlab.informatika.org/ocw/ocw-backend/provider/storage"
 	"gitlab.informatika.org/ocw/ocw-backend/repository/material"
 	"gitlab.informatika.org/ocw/ocw-backend/repository/transaction"
+	"gitlab.informatika.org/ocw/ocw-backend/service/logger"
+	"gitlab.informatika.org/ocw/ocw-backend/utils/env"
+	"gorm.io/gorm"
 )
 
 type MaterialContentServiceImpl struct {
 	transaction.TransactionBuilder
 	material.MaterialContentRepository
+	material.MaterialRepository
+	storage.Storage
+	logger.Logger
+	*env.Environment
+}
+
+func (m MaterialContentServiceImpl) isMaterialContributor(materialId uuid.UUID, email string) error {
+	_, err := m.MaterialRepository.IsUserContributor(materialId, email)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return web.NewResponseError("materials and user combination not found", "NOT_OWNER")
+		}
+
+		return err
+	}
+
+	return nil
 }
 
-func (m MaterialContentServiceImpl) AddContent(materialId uuid.UUID, user user.User, contents []materialDomain.Content) error {
+func (m MaterialContentServiceImpl) AddContent(materialId uuid.UUID, user string, content materialDomain.Content) (string, error) {
+	// TODO : Check user aman ga nambah konten
+	if err := m.isMaterialContributor(materialId, user); err != nil {
+		return "", err
+	}
+
 	isSuccess := false
 	tx := m.Build()
 
 	tx.Begin()
 	defer tx.Auto(&isSuccess)
 
-	// TODO : Check user aman ga nambah konten
+	if content.Type == materialDomain.Handout {
+		path := fmt.Sprintf("%s/%s.pdf", m.BucketMaterialBasePath, strings.ReplaceAll(uuid.New().String(), "-", ""))
+		uploadLink, err := m.Storage.CreatePutSignedLink(context.Background(), path)
 
-	for _, content := range contents {
-		_, err := m.MaterialContentRepository.NewWithTransaction(tx, materialId, content.Type, content.Link)
+		if err != nil {
+			m.Logger.Error("Some error happened when generate link")
+			m.Logger.Error(err.Error())
+			return "", err
+		}
+
+		_, err = m.MaterialContentRepository.NewWithTransaction(tx, materialId, content.Type, path)
 
 		if err != nil {
-			return err
+			return "", err
 		}
-	}
 
-	isSuccess = true
-	return nil
+		isSuccess = true
+
+		return uploadLink, nil
+	} else {
+		if content.Link == "" {
+			return "", web.NewResponseError("content is empty", "ERR_CONTENT_LINK_EMPTY")
+		}
+
+		_, err := m.MaterialContentRepository.NewWithTransaction(tx, materialId, content.Type, content.Link)
+
+		if err == nil {
+			isSuccess = true
+		}
+
+		return "", err
+	}
 }
 
 func (m MaterialContentServiceImpl) DeleteContent(
-	materialId uuid.UUID, user user.User, contentId uuid.UUID,
+	materialId uuid.UUID, user string, contentId uuid.UUID,
 ) error {
 	// TODO: check user aman ga delete konten
-	return m.MaterialContentRepository.Delete(contentId)
-}
+	if err := m.isMaterialContributor(materialId, user); err != nil {
+		return err
+	}
 
-func (m MaterialContentServiceImpl) UpdateContentLink(materialId uuid.UUID, user user.User, contentId uuid.UUID, link string) error {
-	// TODO: Check user aman ga update link
-	return m.MaterialContentRepository.UpdateLink(contentId, link)
+	return m.MaterialContentRepository.Delete(contentId)
 }
diff --git a/service/material/impl.go b/service/material/impl.go
index 1e7c95ff7014ec943f19982a4b95892baa52e2bf..5d59d826c1d80689e479b198b9b714ec94ef1bd2 100644
--- a/service/material/impl.go
+++ b/service/material/impl.go
@@ -1,11 +1,14 @@
 package material
 
 import (
+	"errors"
+
 	"github.com/google/uuid"
-	materialRepo "gitlab.informatika.org/ocw/ocw-backend/model/domain/material"
-	"gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
+	materialDomain "gitlab.informatika.org/ocw/ocw-backend/model/domain/material"
+	"gitlab.informatika.org/ocw/ocw-backend/model/web"
 	"gitlab.informatika.org/ocw/ocw-backend/repository/material"
 	"gitlab.informatika.org/ocw/ocw-backend/repository/transaction"
+	"gorm.io/gorm"
 )
 
 type MaterialServiceImpl struct {
@@ -14,32 +17,39 @@ type MaterialServiceImpl struct {
 	material.MaterialRepository
 }
 
-func (m MaterialServiceImpl) Create(courseId string, user user.User, contents []materialRepo.Content) (uuid.UUID, error) {
+func (m MaterialServiceImpl) Get(courseId string) ([]materialDomain.Material, error) {
+	materials, err := m.MaterialRepository.GetAll(courseId)
+	return materials, err
+}
+
+func (m MaterialServiceImpl) Create(courseId string, email string, name string) (uuid.UUID, error) {
 	isSuccess := false
 	tx := m.TransactionBuilder.Build()
 
 	tx.Begin()
 	defer tx.Auto(&isSuccess)
 
-	id, err := m.MaterialRepository.NewWithTransaction(tx, courseId, user.Email)
+	id, err := m.MaterialRepository.NewWithTransaction(tx, courseId, email, name)
 
 	if err != nil {
 		return uuid.Nil, err
 	}
 
-	for _, content := range contents {
-		_, err = m.MaterialContentRepository.NewWithTransaction(tx, id, content.Type, content.Link)
-
-		if err != nil {
-			return uuid.Nil, err
-		}
-	}
-
 	isSuccess = true
 	return id, err
 }
 
-func (m MaterialServiceImpl) Delete(materialId uuid.UUID, user user.User) error {
+func (m MaterialServiceImpl) Delete(materialId uuid.UUID, email string) error {
 	// TODO: Pengecekan user apakah kontributor course bukan
+	_, err := m.MaterialRepository.IsUserContributor(materialId, email)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return web.NewResponseError("User is not the owner of material", "NOT_OWNER")
+		}
+
+		return err
+	}
+
 	return m.MaterialRepository.Delete(materialId)
 }
diff --git a/service/material/type.go b/service/material/type.go
index 66fe99b49e176c375812c020389aaf855fb3d7bb..1e129d48229c705cd961b4cb8d33c35459b414b9 100644
--- a/service/material/type.go
+++ b/service/material/type.go
@@ -3,16 +3,15 @@ package material
 import (
 	"github.com/google/uuid"
 	"gitlab.informatika.org/ocw/ocw-backend/model/domain/material"
-	"gitlab.informatika.org/ocw/ocw-backend/model/domain/user"
 )
 
 type MaterialService interface {
-	Create(courseId string, user user.User, contents []material.Content) (uuid.UUID, error)
-	Delete(materialId uuid.UUID, user user.User) error
+	Create(courseId string, email string, name string) (uuid.UUID, error)
+	Delete(materialId uuid.UUID, email string) error
+	Get(courseId string) ([]material.Material, error)
 }
 
 type MaterialContentService interface {
-	AddContent(materialId uuid.UUID, user user.User, contents []material.Content) error
-	DeleteContent(materialId uuid.UUID, user user.User, contentId uuid.UUID) error
-	UpdateContentLink(materialId uuid.UUID, user user.User, contentId uuid.UUID, link string) error
+	AddContent(materialId uuid.UUID, email string, content material.Content) (string, error)
+	DeleteContent(materialId uuid.UUID, email string, contentId uuid.UUID) error
 }
diff --git a/utils/env/env.go b/utils/env/env.go
index f53df6088f224fa4822983591509d93e7f4e93c4..ff09c00b0ff32c969ddd850d791ca8e35ad8fc9d 100644
--- a/utils/env/env.go
+++ b/utils/env/env.go
@@ -62,7 +62,9 @@ type Environment struct {
 	BucketSignedPutDuration int64 `env:"BUCKET_SIGNED_PUT_DURATION_S" envDefault:"36000"`
 	BucketSignedGetDuration int64 `env:"BUCKET_SIGNED_GET_DURATION_S" envDefault:"1800"`
 
-	BucketMaterialBasePath string `env:"BUCKET_MATERIAL_BASE_PATH" envDefault:"materials/"`
+	BucketMaterialBasePath string `env:"BUCKET_MATERIAL_BASE_PATH" envDefault:"materials"`
+
+	UseBucket bool `env:"USE_BUCKET" envDefault:"true"`
 }
 
 func New() (*Environment, error) {