diff --git a/src/main/java/nl/tudelft/queue/controller/AssignmentController.java b/src/main/java/nl/tudelft/queue/controller/AssignmentController.java
new file mode 100644
index 0000000000000000000000000000000000000000..867281f1232414154cd1b33b2484676123aab16f
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/controller/AssignmentController.java
@@ -0,0 +1,158 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.controller;
+
+import nl.tudelft.labracore.api.AssignmentControllerApi;
+import nl.tudelft.labracore.api.dto.AssignmentCreateDTO;
+import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
+import nl.tudelft.labracore.api.dto.ModuleDetailsDTO;
+import nl.tudelft.queue.cache.AssignmentCacheManager;
+import nl.tudelft.queue.cache.EditionCacheManager;
+import nl.tudelft.queue.cache.ModuleCacheManager;
+import nl.tudelft.queue.dto.create.QueueAssignmentCreateDTO;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+
+@Controller
+public class AssignmentController {
+
+	@Autowired
+	private AssignmentCacheManager aCache;
+
+	@Autowired
+	private ModuleCacheManager mCache;
+
+	@Autowired
+	private EditionCacheManager eCache;
+
+	@Autowired
+	private AssignmentControllerApi aApi;
+
+	/**
+	 * Gets the page for creating an assignment. This is a basic page with only the most basic of properties
+	 * for an assignment. Further adjustments or creation options should be made available in Portal or later
+	 * in Queue.
+	 *
+	 * @param  moduleId The id of the module to add the assignment to.
+	 * @param  model    The model to fill out for Thymeleaf template resolution.
+	 * @return          The Thymeleaf template to resolve.
+	 */
+	@GetMapping("/module/{moduleId}/assignment/create")
+	@PreAuthorize("@permissionService.canManageModule(#moduleId)")
+	public String getAssignmentCreatePage(@PathVariable Long moduleId,
+			Model model) {
+		return addCreateAssignmentAttributes(moduleId, new QueueAssignmentCreateDTO(moduleId), model);
+	}
+
+	/**
+	 * Handles a Post request to add a new assignment. The assignment is passed through a
+	 * {@link QueueAssignmentCreateDTO}. After adding the assignment successfully, the user is redirected back
+	 * to the edition/modules page. If something goes wrong during conversion, the user is instead directed
+	 * back to the assignment create page.
+	 *
+	 * @param  mId   The id of the module to add the assignment to.
+	 * @param  dto   The dto containing information for creating the assignment.
+	 * @param  model The model to fill out for Thymeleaf template resolution.
+	 * @return       A redirect back to the edition/modules page.
+	 */
+	@PostMapping("/module/{mId}/assignment/create")
+	@PreAuthorize("@permissionService.canManageModule(#mId)")
+	public String createAssignment(@PathVariable Long mId,
+			QueueAssignmentCreateDTO dto,
+			Model model) {
+		dto.setModuleId(mId);
+
+		AssignmentCreateDTO create = dto.apply();
+		if (dto.hasErrors()) {
+			return addCreateAssignmentAttributes(mId, dto, model);
+		}
+
+		aApi.addAssignment(create).block();
+
+		ModuleDetailsDTO module = mCache.getOrThrow(mId);
+		return "redirect:/edition/" + module.getEdition().getId() + "/modules";
+	}
+
+	/**
+	 * Gets the assignment removal page. This page is simply to confirm whether the user really wants to
+	 * delete the assignment with the given id.
+	 *
+	 * @param  assignmentId The id of the assignment that the user might want to delete.
+	 * @param  model        The model to fill out for Thymeleaf template resolution.
+	 * @return              The Thymeleaf template to resolve.
+	 */
+	@GetMapping("/assignment/{assignmentId}/remove")
+	@PreAuthorize("@permissionService.canManageAssignment(#assignmentId)")
+	public String getAssignmentRemovePage(@PathVariable Long assignmentId,
+			Model model) {
+		var assignment = aCache.getOrThrow(assignmentId);
+		var module = mCache.getOrThrow(assignment.getModule().getId());
+		var edition = eCache.getOrThrow(module.getEdition().getId());
+
+		model.addAttribute("edition", edition);
+		model.addAttribute("assignment", assignment);
+
+		return "assignment/remove";
+	}
+
+	/**
+	 * Deletes the assignment with the given id and redirects back to the modules page for the relevant
+	 * edition.
+	 *
+	 * @param  assignmentId The id of the assignment to delete.
+	 * @return              A redirect back to the edition/modules page.
+	 */
+	@PostMapping("/assignment/{assignmentId}/remove")
+	@PreAuthorize("@permissionService.canManageAssignment(#assignmentId)")
+	public String removeAssignment(@PathVariable Long assignmentId) {
+		var assignment = aCache.getOrThrow(assignmentId);
+		var module = mCache.getOrThrow(assignment.getModule().getId());
+
+		// TODO: Remove the assignment
+
+		return "redirect:/edition/" + module.getEdition().getId() + "/modules";
+	}
+
+	/**
+	 * Adds attributes for the create assignment page to the given model.
+	 *
+	 * @param  moduleId The id of the module the assignment will be a part of.
+	 * @param  dto      The DTO to add to the model for the page form to fill out.
+	 * @param  model    The model to fill with attributes for the create-assignment page.
+	 * @return          The template to load for the create assignments page.
+	 */
+	private String addCreateAssignmentAttributes(Long moduleId, QueueAssignmentCreateDTO dto,
+			Model model) {
+		ModuleDetailsDTO module = mCache.getOrThrow(moduleId);
+		EditionDetailsDTO edition = eCache.getOrThrow(module.getEdition().getId());
+
+		model.addAttribute("edition", edition);
+		model.addAttribute("_module", module);
+
+		model.addAttribute("dto", dto);
+
+		return "assignment/create";
+	}
+
+}
diff --git a/src/main/java/nl/tudelft/queue/controller/EditionController.java b/src/main/java/nl/tudelft/queue/controller/EditionController.java
index e86388f5738691d42a868e7e078e95c332b95403..44e6f7bd4c3291cc1f1add36a897929f0b7fdbdf 100644
--- a/src/main/java/nl/tudelft/queue/controller/EditionController.java
+++ b/src/main/java/nl/tudelft/queue/controller/EditionController.java
@@ -25,9 +25,7 @@ import java.util.List;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
-import nl.tudelft.labracore.api.EditionControllerApi;
-import nl.tudelft.labracore.api.PersonControllerApi;
-import nl.tudelft.labracore.api.RoleControllerApi;
+import nl.tudelft.labracore.api.*;
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
 import nl.tudelft.labracore.lib.security.user.Person;
@@ -299,6 +297,9 @@ public class EditionController {
 	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
 	public String createParticipant(@PathVariable Long editionId, Model model,
 			QueueRoleCreateDTO dto) {
+		// TODO: Figure out a way to get rid of these additional sets
+		dto.setEditionId(editionId);
+
 		RoleCreateDTO create = dto.apply();
 		if (dto.hasErrors()) {
 			model.addAttribute("edition", eCache.getOrThrow(editionId));
diff --git a/src/main/java/nl/tudelft/queue/controller/ModuleController.java b/src/main/java/nl/tudelft/queue/controller/ModuleController.java
new file mode 100644
index 0000000000000000000000000000000000000000..49773dbc2a268c1ca0ab403048f7877eb0f35989
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/controller/ModuleController.java
@@ -0,0 +1,140 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.controller;
+
+import nl.tudelft.labracore.api.ModuleControllerApi;
+import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
+import nl.tudelft.labracore.api.dto.ModuleCreateDTO;
+import nl.tudelft.queue.cache.EditionCacheManager;
+import nl.tudelft.queue.cache.ModuleCacheManager;
+import nl.tudelft.queue.dto.create.QueueModuleCreateDTO;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+
+@Controller
+public class ModuleController {
+
+	@Autowired
+	private ModuleCacheManager mCache;
+
+	@Autowired
+	private EditionCacheManager eCache;
+
+	@Autowired
+	private ModuleControllerApi mApi;
+
+	/**
+	 * Gets the creation page for a module in the given edition. This sets up page attributes and returns the
+	 * thymeleaf template to resolve.
+	 *
+	 * @param  editionId The id of the edition the module should be in.
+	 * @param  model     The model to fill out for Thymeleaf template resolution.
+	 * @return           The Thymeleaf template to resolve.
+	 */
+	@GetMapping("/edition/{editionId}/modules/create")
+	@PreAuthorize("@permissionService.canManageEdition(#editionId)")
+	public String getModuleCreatePage(@PathVariable Long editionId, Model model) {
+		return addCreateModuleAttributes(editionId, new QueueModuleCreateDTO(editionId), model);
+	}
+
+	/**
+	 * Creates a module from the given DTO in the edition with the given id.
+	 *
+	 * @param  editionId The id of the edition to create the module in.
+	 * @param  dto       The creation DTO containing all information needed to create a new module.
+	 * @param  model     The model to fill out for Thymeleaf template resolution.
+	 * @return           A redirect back to the modules page or the module create page if something goes
+	 *                   wrong.
+	 */
+	@PostMapping("/edition/{editionId}/modules/create")
+	@PreAuthorize("@permissionService.canManageEdition(#editionId)")
+	public String createModule(@PathVariable Long editionId, QueueModuleCreateDTO dto, Model model) {
+		// Ensure the DTO contains the right edition ID, as malicious users can inject a different ID in the form
+		dto.setEditionId(editionId);
+
+		ModuleCreateDTO create = dto.apply();
+		if (dto.hasErrors()) {
+			return addCreateModuleAttributes(editionId, dto, model);
+		}
+
+		mApi.addModule(create).block();
+
+		return "redirect:/edition/" + editionId + "/modules";
+	}
+
+	/**
+	 * Gets the module removal page. This page is only to confirm that the user does indeed want to remove the
+	 * module and all attached assignments/student groups.
+	 *
+	 * @param  moduleId The id of the module to remove.
+	 * @param  model    The model to fill out for Thymeleaf template resolution.
+	 * @return          The Thymeleaf template to resolve.
+	 */
+	@GetMapping("/module/{moduleId}/remove")
+	public String getModuleRemovePage(@PathVariable Long moduleId,
+			Model model) {
+		var module = mCache.getOrThrow(moduleId);
+		var edition = eCache.getOrThrow(module.getEdition().getId());
+
+		model.addAttribute("edition", edition);
+		model.addAttribute("_module", module);
+
+		return "module/remove";
+	}
+
+	/**
+	 * Removes the module with the given id.
+	 *
+	 * @param  moduleId The id of the module to delete.
+	 * @return          A redirect back to the edition modules page.
+	 */
+	@PostMapping("/module/{moduleId}/remove")
+	public String removeModule(@PathVariable Long moduleId) {
+		var module = mCache.getOrThrow(moduleId);
+		var edition = eCache.getOrThrow(module.getEdition().getId());
+
+		// TODO: Remove the module
+
+		return "redirect:/edition/" + edition.getId() + "/modules";
+	}
+
+	/**
+	 * Fills in page attributes for the module creation page with the id of the edition and the initial module
+	 * creation DTO object.
+	 *
+	 * @param  editionId The id of the edition to add the module to.
+	 * @param  dto       The initial creation DTO for the module.
+	 * @param  model     The model to fill out for Thymeleaf template resolution.
+	 * @return           The Thymeleaf template to resolve.
+	 */
+	private String addCreateModuleAttributes(Long editionId, QueueModuleCreateDTO dto, Model model) {
+		EditionDetailsDTO edition = eCache.getOrThrow(editionId);
+
+		model.addAttribute("edition", edition);
+		model.addAttribute("dto", dto);
+
+		return "module/create";
+	}
+
+}
diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..7f329e5ad765c9e5017853c497e3e6541d5adaba
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java
@@ -0,0 +1,67 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.dto.create;
+
+import java.time.LocalDateTime;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+import lombok.*;
+import nl.tudelft.labracore.api.dto.AssignmentCreateDTO;
+import nl.tudelft.labracore.api.dto.Description;
+import nl.tudelft.labracore.api.dto.ModuleIdDTO;
+import nl.tudelft.librador.dto.create.Create;
+
+import org.springframework.format.annotation.DateTimeFormat;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+public class QueueAssignmentCreateDTO extends Create<AssignmentCreateDTO> {
+	private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm";
+
+	@NotBlank
+	private String name;
+	@NotNull
+	@Builder.Default
+	private String description = "";
+
+	@DateTimeFormat(pattern = TIME_FORMAT)
+	private LocalDateTime deadline;
+
+	@NotNull
+	private Long moduleId;
+
+	public QueueAssignmentCreateDTO(Long moduleId) {
+		this.moduleId = moduleId;
+	}
+
+	@Override
+	public Class<AssignmentCreateDTO> clazz() {
+		return AssignmentCreateDTO.class;
+	}
+
+	@Override
+	protected void postApply(AssignmentCreateDTO data) {
+		data.description(new Description().text(description));
+		data.module(new ModuleIdDTO().id(moduleId));
+	}
+}
diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..bc56b13233759aa4328c9e3e14a30ab24e015a25
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java
@@ -0,0 +1,51 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.dto.create;
+
+import javax.validation.constraints.NotBlank;
+
+import lombok.*;
+import nl.tudelft.labracore.api.dto.EditionIdDTO;
+import nl.tudelft.labracore.api.dto.ModuleCreateDTO;
+import nl.tudelft.librador.dto.create.Create;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+public class QueueModuleCreateDTO extends Create<ModuleCreateDTO> {
+	private Long editionId;
+
+	@NotBlank
+	private String name;
+
+	public QueueModuleCreateDTO(Long editionId) {
+		this.editionId = editionId;
+	}
+
+	@Override
+	public Class<ModuleCreateDTO> clazz() {
+		return ModuleCreateDTO.class;
+	}
+
+	@Override
+	protected void postApply(ModuleCreateDTO data) {
+		data.edition(new EditionIdDTO().id(editionId));
+	}
+}
diff --git a/src/main/java/nl/tudelft/queue/service/PermissionService.java b/src/main/java/nl/tudelft/queue/service/PermissionService.java
index c68f465b0241954504bb8c802062c99f0684982e..88154eec0e655db2135ae30ade637d4d110a78f1 100644
--- a/src/main/java/nl/tudelft/queue/service/PermissionService.java
+++ b/src/main/java/nl/tudelft/queue/service/PermissionService.java
@@ -30,6 +30,7 @@ import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.labracore.lib.security.LabradorUserDetails;
 import nl.tudelft.labracore.lib.security.user.DefaultRole;
 import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.queue.cache.*;
 import nl.tudelft.queue.cache.EditionCollectionCacheManager;
 import nl.tudelft.queue.cache.RoleCacheManager;
 import nl.tudelft.queue.cache.SessionCacheManager;
@@ -63,11 +64,17 @@ public class PermissionService {
 	private RequestRepository rr;
 
 	@Autowired
-	private RoleCacheManager rCache;
+	private AssignmentCacheManager aCache;
+
+	@Autowired
+	private ModuleCacheManager mCache;
 
 	@Autowired
 	private EditionCollectionCacheManager ecCache;
 
+	@Autowired
+	private RoleCacheManager rCache;
+
 	@Autowired
 	private SessionCacheManager sCache;
 
@@ -134,6 +141,31 @@ public class PermissionService {
 		return lr.findById(labId).map(f).orElse(false);
 	}
 
+	/**
+	 * Checks whether a predicate holds given the module with the given id. If no such module could be found,
+	 * this function just outputs {@code false}.
+	 *
+	 * @param  moduleId The id of the module to find.
+	 * @param  f        The predicate to check.
+	 * @return          The outcome of the predicate, or false if no module with the given id could be found.
+	 */
+	private boolean withModule(Long moduleId, Function<ModuleDetailsDTO, Boolean> f) {
+		return mCache.get(moduleId).map(f).orElse(false);
+	}
+
+	/**
+	 * Checks whether a predicate holds given the assignment with the given id. If no such assignment could be
+	 * found, this function just outputs {@code false}.
+	 *
+	 * @param  assignmentId The id of the assignment to find.
+	 * @param  f            The predicate to check.
+	 * @return              The outcome of the predicate, or false if no assignment with the given id could be
+	 *                      found.
+	 */
+	private boolean withAssignment(Long assignmentId, Function<AssignmentDetailsDTO, Boolean> f) {
+		return aCache.get(assignmentId).map(f).orElse(false);
+	}
+
 	/**
 	 * Checks whether a predicate holds given the session with the given id. If no such session could be
 	 * found, this function just outputs {@code false}.
@@ -391,13 +423,27 @@ public class PermissionService {
 
 	/**
 	 * @param  lab The lab that the user wants to manage.
-	 * @return     Whether the authenticated user can change values in the given lab.
+	 * @return     Whether the authenticated user can change properties of the given lab.
 	 */
 	public boolean canManageLab(Lab lab) {
 		return isAdmin() || withEditions(lab,
 				editionIds -> withAnyRole(editionIds, (p, t) -> MANAGER_ROLES.contains(t)));
 	}
 
+	/**
+	 * @param  moduleId The id of the module that the user wants to manage.
+	 * @return          Whether the authenticated user can change properties of the given module and add
+	 *                  assignments/groups.
+	 */
+	public boolean canManageModule(Long moduleId) {
+		return isAdmin() || withModule(moduleId, module -> canManageEdition(module.getEdition().getId()));
+	}
+
+	public boolean canManageAssignment(Long assignmentId) {
+		return isAdmin() || withAssignment(assignmentId,
+				assignment -> canManageModule(assignment.getModule().getId()));
+	}
+
 	/**
 	 * @param  editionId The id of the edition the user wants to manage teachers for.
 	 * @return           Whether the authenticated user can add and remove teachers to and from the edition
diff --git a/src/main/resources/templates/assignment/create.html b/src/main/resources/templates/assignment/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..d81ae658b28337e69571c2258c919d64f9f6c35d
--- /dev/null
+++ b/src/main/resources/templates/assignment/create.html
@@ -0,0 +1,99 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+<head>
+    <title th:text="|Create Assignment for ${'#' + edition.id}|"></title>
+
+    <script src="/webjars/momentjs/min/moment.min.js"></script>
+
+    <script type="application/javascript" src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script>
+    <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/>
+
+    <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/>
+</head>
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="_module" type="nl.tudelft.labracore.api.dto.ModuleSummaryDTO"-->
+
+<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.QueueAssignmentCreateDTO"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-header">
+        <h3>Create module</h3>
+    </div>
+
+    <form th:with="action = @{/module/{mId}/assignment/create(id=${edition.id}, mId=${_module.id})}"
+          th:action="${action}" th:object="${dto}"
+          class="form-horizontal" method="post">
+        <input type="hidden" name="moduleId" th:value="${_module.id}">
+
+        <div class="form-group form-row">
+            <label for="name-input" class="col-sm-2 col-form-label">Name:</label>
+            <div class="col-sm-4">
+                <input id="name-input"
+                       type="text" class="form-control"
+                       required th:field="*{name}"/>
+            </div>
+        </div>
+
+        <div class="form-group form-row">
+            <label for="description-input" class="col-sm-2 col-form-label">Description:</label>
+            <div class="col-sm-10">
+                <input id="description-input"
+                       type="text" class="form-control"
+                       th:field="*{description}"/>
+            </div>
+        </div>
+
+        <div class="form-group form-row">
+            <label for="deadline-input" class="col-sm-2 col-form-label">Deadline:</label>
+            <div class="col-sm-10 input-group" id="deadline-input" data-target-input="nearest">
+                <input type="text" th:field="*{deadline}"
+                       class="form-control datetimepicker-input"
+                       data-target="#deadline-input"/>
+                <div class="input-group-append">
+                    <span class="input-group-text"
+                          data-target="#deadline-input"
+                          data-toggle="datetimepicker">
+                        <i class="fa fa-calendar"></i>
+                    </span>
+                </div>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <button type="submit" class="btn btn-primary ctrl-enter-submit float-right">
+                Create new Assignment
+            </button>
+            <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
+        </div>
+    </form>
+
+    <script type="application/javascript">
+        $("#deadline-input").datetimepicker({format: "DD-MM-YYYY HH:mm"})
+    </script>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/assignment/remove.html b/src/main/resources/templates/assignment/remove.html
new file mode 100644
index 0000000000000000000000000000000000000000..d13122753b3c36d52c0f299878094e30dff16c41
--- /dev/null
+++ b/src/main/resources/templates/assignment/remove.html
@@ -0,0 +1,47 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="assignment" type="nl.tudelft.labracore.api.dto.AssignmentDetailsDTO"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-sub-header">
+        <h3>Remove Assignment</h3>
+    </div>
+
+    <form th:action="@{/assignment/{id}/remove(id=${assignment.id})}"
+          class="form-horizontal"
+          method="post">
+        Are you sure you want to remove <strong
+            th:text="${'assignment #' + assignment.id + ' (' + assignment.name + ')'}"></strong>?
+        <div class="text-center">
+            <button class="btn btn-danger">Delete this lab</button>
+            <small>or <a
+                    th:href="@{/edition/{eId}/modules(eId=${edition.id})}">go back</a></small>
+        </div>
+    </form>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/edition/view/labs.html b/src/main/resources/templates/edition/view/labs.html
index a8e138de429cb2825a332e13e6921b5f492dfeed..a43f2688abacf14e3da89302f44abfb25a92793c 100644
--- a/src/main/resources/templates/edition/view/labs.html
+++ b/src/main/resources/templates/edition/view/labs.html
@@ -44,7 +44,7 @@
             <a th:each="lType : ${T(nl.tudelft.queue.model.enums.LabType).values()}"
                     href="#" th:href="@{/edition/{id}/lab/create(id=${edition.id}, type=${lType.name()})}"
                th:text="|Create ${lType.displayName} Lab|"
-               class="btn btn-sm btn-primary mr-1">
+               class="btn btn-primary mr-1">
             </a>
         </div>
         <h3>Labs</h3>
diff --git a/src/main/resources/templates/edition/view/modules.html b/src/main/resources/templates/edition/view/modules.html
index a51ef4f212a9060eb0ec99886c67951e8db3233f..9a38291a8d20bfc86da789fcef1baaf532c6d437 100644
--- a/src/main/resources/templates/edition/view/modules.html
+++ b/src/main/resources/templates/edition/view/modules.html
@@ -23,7 +23,7 @@
 
 <!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
 
-<!--@thymesVar id="modules" type="nl.tudelft.labracore.api.dto.ModuleDetailsDTO"-->
+<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"-->
 <!--@thymesVar id="groups", type="java.util.Map<java.lang.Long, java.util.List<nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO>>"-->
 
 <head>
@@ -33,6 +33,10 @@
 <body>
 <section layout:fragment="subcontent">
     <div class="page-sub-header">
+        <a class="btn btn-primary float-right"
+           th:href="@{/edition/{id}/modules/create(id=${edition.id})}">
+            Create Module
+        </a>
         <h3>Modules</h3>
     </div>
 
@@ -43,11 +47,11 @@
     <div th:unless="${#lists.isEmpty(modules)}" class="row">
         <div class="col-md-3 mb-2">
             <div class="list-group" role="tablist">
-                <th:block th:each="module, idx : ${modules}">
+                <th:block th:each="_module, idx : ${modules}">
                     <a class="list-group-item list-group-item-action"
                        th:classappend="${idx.first ? 'active' : ''}"
                        role="tab" data-toggle="list"
-                       th:href="'#module-' + ${module.id}" th:text="${module.name}"></a>
+                       th:href="'#module-' + ${_module.id}" th:text="${_module.name}"></a>
                 </th:block>
             </div>
         </div>
@@ -59,25 +63,49 @@
                      role="tabpanel"
                      th:id="'module-' + ${m.id}">
 
-                    <h3 class="ml-2">Assignments</h3>
+                    <div>
+                        <a class="btn btn-danger btn-sm float-right disabled"
+                           th:href="@{/module/{mId}/remove(mId=${m.id})}"
+                           th:text="|Remove module - ${m.name}|"
+                           style="pointer-events: all !important;"
+                           data-toggle="tooltip" data-placement="top" title="Not available yet">
+                        </a>
+                    </div>
+
+                    <div>
+                        <a class="btn btn-primary btn-sm float-right mr-4"
+                           th:href="@{/module/{mId}/assignment/create(mId=${m.id})}">
+                            Create Assignment
+                        </a>
+                        <h3 class="ml-2">Assignments</h3>
+                    </div>
 
                     <h5 th:if="${#lists.isEmpty(m.assignments)}">
                         There are no assignments in this module, configure them in Portal
                     </h5>
 
-                    <div class="row col-12" th:unless="${#lists.isEmpty(m.assignments)}">
+                    <div class="row col-12 mt-2" th:unless="${#lists.isEmpty(m.assignments)}">
                         <table class="table">
                             <thead class="thead-light">
                             <tr>
                                 <th scope="col">#</th>
                                 <th scope="col">Name</th>
                                 <th scope="col">Description</th>
+                                <th scope="col"></th>
                             </tr>
                             </thead>
                             <tr th:each="assignment, aIdx : ${m.getAssignments()}">
                                 <th scope="row" th:text="${assignment.id}"></th>
                                 <td th:text="${assignment.name}"></td>
                                 <td th:text="${assignment.getDescription().text}"></td>
+                                <td>
+                                    <a th:href="@{/assignment/{aId}/remove(aId=${assignment.id})}"
+                                       style="pointer-events: all !important;"
+                                       class="btn btn-danger float-right to-be-implemented disabled"
+                                       data-toggle="tooltip" data-placement="top" title="Not available yet">
+                                        <i class="fa fa-trash"></i>
+                                    </a>
+                                </td>
                             </tr>
                         </table>
                     </div>
@@ -115,6 +143,12 @@
             </div>
         </div>
     </div>
+
+    <script>
+        $(function() {
+            $("[data-toggle='tooltip']").tooltip()
+        })
+    </script>
 </section>
 </body>
 </html>
diff --git a/src/main/resources/templates/edition/view/participants.html b/src/main/resources/templates/edition/view/participants.html
index 8582e0c0a115d26474e58d8085a9a08f4efeaef8..a786b2581d13a776dba4f38f94612ca5eb7e5803 100644
--- a/src/main/resources/templates/edition/view/participants.html
+++ b/src/main/resources/templates/edition/view/participants.html
@@ -34,7 +34,7 @@
     <div class="page-sub-header">
         <div class="float-right" style="margin-right: 10px">
             <a href="#" th:href="@{/edition/{id}/participants/create(id=${edition.id})}"
-               class="btn btn-sm btn-secondary">
+               class="btn btn-secondary">
                 Add participant
             </a>
         </div>
diff --git a/src/main/resources/templates/lab/create.html b/src/main/resources/templates/lab/create.html
index 6da34ff0cd739ed13edfb71ee2769e61ccd53848..dc77ddbe4d462bf17e3bc4a71d1c0cb18dc04f41 100644
--- a/src/main/resources/templates/lab/create.html
+++ b/src/main/resources/templates/lab/create.html
@@ -21,6 +21,8 @@
 <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
       layout:decorate="~{edition/view}">
 <head>
+    <title th:text="|Create Lab for ${'#' + (ec == null ? edition.id : ec.id)}|"></title>
+
     <script src="/webjars/momentjs/min/moment.min.js"></script>
 
     <script type="application/javascript"
diff --git a/src/main/resources/templates/module/create.html b/src/main/resources/templates/module/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..de51cf26b1348195cfeec64370fcdbb35c40db86
--- /dev/null
+++ b/src/main/resources/templates/module/create.html
@@ -0,0 +1,64 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+<head>
+    <title th:text="|Create Module for ${'#' + edition.id}|"></title>
+
+    <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/>
+</head>
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.QueueModuleCreateDTO"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-header">
+        <h3>Create module</h3>
+    </div>
+
+    <form th:with="action = @{/edition/{id}/modules/create(id=${edition.id})}"
+          th:action="${action}" th:object="${dto}"
+          class="form-horizontal" method="post">
+        <input type="hidden" name="id" th:value="${edition.id}">
+
+        <div class="form-group form-row">
+            <label for="title-input" class="col-md-2 col-form-label">Name:</label>
+            <div class="col-md-4">
+                <input id="title-input"
+                       type="text" class="form-control"
+                       required="required" th:field="*{name}"
+                       placeholder="Assignments"/>
+            </div>
+
+            <div class="col-md-6">
+                <button type="submit" class="btn btn-primary ctrl-enter-submit float-right">
+                    Create new module
+                </button>
+                <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
+            </div>
+        </div>
+    </form>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/module/remove.html b/src/main/resources/templates/module/remove.html
new file mode 100644
index 0000000000000000000000000000000000000000..efc76b372a707d0d11c365303591a786fbef0fb8
--- /dev/null
+++ b/src/main/resources/templates/module/remove.html
@@ -0,0 +1,47 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="_module" type="nl.tudelft.labracore.api.dto.ModuleDetailsDTO"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-sub-header">
+        <h3>Remove Module</h3>
+    </div>
+
+    <form th:action="@{/module/{id}/remove(id=${_module.id})}"
+          class="form-horizontal"
+          method="post">
+        Are you sure you want to remove <strong
+            th:text="${'module #' + _module.id + ' (' + _module.name + ')'}"></strong>?
+        <div class="text-center">
+            <button class="btn btn-danger">Delete this lab</button>
+            <small>or <a
+                    th:href="@{/edition/{eId}/modules(eId=${edition.id})}">go back</a></small>
+        </div>
+    </form>
+</section>
+</body>
+</html>
diff --git a/src/test/java/nl/tudelft/queue/controller/AnnouncementControllerTest.java b/src/test/java/nl/tudelft/queue/controller/AnnouncementControllerTest.java
index 392af3249deb329f72f6530335c20998500cb936..b4dc203ac8a795374228fe5f7a33f95b80a074ba 100644
--- a/src/test/java/nl/tudelft/queue/controller/AnnouncementControllerTest.java
+++ b/src/test/java/nl/tudelft/queue/controller/AnnouncementControllerTest.java
@@ -44,6 +44,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
 
@@ -53,6 +54,7 @@ import test.test.TestQueueApplication;
 @Transactional
 @AutoConfigureMockMvc
 @SpringBootTest(classes = TestQueueApplication.class)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
 public class AnnouncementControllerTest {
 
 	@Autowired
diff --git a/src/test/java/nl/tudelft/queue/controller/AssignmentControllerTest.java b/src/test/java/nl/tudelft/queue/controller/AssignmentControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8b86ca6d1ecb9a0b38f3a0e96a05d034aa4aea0c
--- /dev/null
+++ b/src/test/java/nl/tudelft/queue/controller/AssignmentControllerTest.java
@@ -0,0 +1,151 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.controller;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import javax.transaction.Transactional;
+
+import nl.tudelft.labracore.api.AssignmentControllerApi;
+import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.queue.cache.AssignmentCacheManager;
+import nl.tudelft.queue.cache.EditionCacheManager;
+import nl.tudelft.queue.cache.ModuleCacheManager;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+
+import reactor.core.publisher.Mono;
+import test.test.TestQueueApplication;
+
+@Transactional
+@AutoConfigureMockMvc
+@SpringBootTest(classes = TestQueueApplication.class)
+public class AssignmentControllerTest {
+
+	@Autowired
+	private MockMvc mvc;
+
+	@Autowired
+	private AssignmentCacheManager aCache;
+
+	@Autowired
+	private ModuleCacheManager mCache;
+
+	@Autowired
+	private EditionCacheManager eCache;
+
+	@Autowired
+	private AssignmentControllerApi aApi;
+
+	private final EditionDetailsDTO edition1 = new EditionDetailsDTO()
+			.id(33L).name("Edition").course(new CourseSummaryDTO().id(8732L).code("EDED").name("Coursy"));
+
+	private final ModuleDetailsDTO module1 = new ModuleDetailsDTO()
+			.id(53L).name("Module 1").edition(new EditionSummaryDTO().id(33L));
+
+	private final AssignmentDetailsDTO assignment1 = new AssignmentDetailsDTO()
+			.id(98732L).name("Assignment 1").acceptLateSubmissions(false)
+			.module(new ModuleSummaryDTO().id(53L));
+
+	@BeforeEach
+	void setUp() {
+		when(aCache.getOrThrow(eq(assignment1.getId()))).thenReturn(assignment1);
+		when(mCache.getOrThrow(eq(module1.getId()))).thenReturn(module1);
+		when(eCache.getOrThrow(eq(edition1.getId()))).thenReturn(edition1);
+
+		when(aApi.addAssignment(any())).thenReturn(Mono.just(666L));
+	}
+
+	@Test
+	@WithUserDetails("admin1")
+	void getAssignmentCreatePageLoads() throws Exception {
+		mvc.perform(get("/module/{moduleId}/assignment/create", module1.getId()))
+				.andExpect(status().isOk())
+				.andExpect(view().name("assignment/create"))
+				.andExpect(model().attributeExists("dto", "_module", "edition"));
+	}
+
+	@Test
+	@WithUserDetails("admin1")
+	void createAssignmentCreatesANewAssignment() throws Exception {
+		mvc.perform(post("/module/{moduleId}/assignment/create", module1.getId()).with(csrf())
+				.queryParam("name", "Assignment 2")
+				.queryParam("description", "This is a cool assignment duh")
+				.queryParam("deadline", "09-09-2022 12:00")
+				.queryParam("moduleId", "53"))
+				.andExpect(redirectedUrl("/edition/33/modules"));
+
+		verify(aApi).addAssignment(eq(new AssignmentCreateDTO()
+				.name("Assignment 2")
+				.description(new Description().text("This is a cool assignment duh"))
+				.deadline(LocalDateTime.of(2022, 9, 9, 12, 0))
+				.module(new ModuleIdDTO().id(53L))));
+	}
+
+	@Test
+	@WithUserDetails("admin1")
+	void getAssignmentRemovePageWorks() throws Exception {
+		mvc.perform(get("/assignment/{assignmentId}/remove", assignment1.getId()))
+				.andExpect(status().isOk())
+				.andExpect(view().name("assignment/remove"))
+				.andExpect(model().attributeExists("edition", "assignment"));
+	}
+
+	@Test
+	@WithUserDetails("admin1")
+	void removeAssignmentRedirectsBackToModulesOverview() throws Exception {
+		mvc.perform(post("/assignment/{assignmentId}/remove", assignment1.getId()).with(csrf()))
+				.andExpect(redirectedUrl("/edition/" + edition1.getId() + "/modules"));
+	}
+
+	@ParameterizedTest
+	@MethodSource(value = "protectedEndpoints")
+	void testWithoutUserDetailsIsForbidden(MockHttpServletRequestBuilder request) throws Exception {
+		mvc.perform(request.with(csrf()))
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("http://localhost/login"));
+	}
+
+	private static List<MockHttpServletRequestBuilder> protectedEndpoints() {
+		return List.of(
+				get("/module/{moduleId}/assignment/create", 1),
+				post("/module/{mId}/assignment/create", 1),
+				get("/assignment/{assignmentId}/remove", 1),
+				post("/assignment/{assignmentId}/remove", 1));
+	}
+
+}
diff --git a/src/test/java/nl/tudelft/queue/controller/ModuleControllerTest.java b/src/test/java/nl/tudelft/queue/controller/ModuleControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7575e1317e77895844551a2d34795c90e11ac992
--- /dev/null
+++ b/src/test/java/nl/tudelft/queue/controller/ModuleControllerTest.java
@@ -0,0 +1,136 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.controller;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+import java.util.List;
+
+import javax.transaction.Transactional;
+
+import nl.tudelft.labracore.api.ModuleControllerApi;
+import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.queue.cache.EditionCacheManager;
+import nl.tudelft.queue.cache.ModuleCacheManager;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+
+import reactor.core.publisher.Mono;
+import test.test.TestQueueApplication;
+
+@Transactional
+@AutoConfigureMockMvc
+@SpringBootTest(classes = TestQueueApplication.class)
+public class ModuleControllerTest {
+
+	@Autowired
+	private MockMvc mvc;
+
+	@Autowired
+	private ModuleCacheManager mCache;
+
+	@Autowired
+	private EditionCacheManager eCache;
+
+	@Autowired
+	private ModuleControllerApi mApi;
+
+	private final EditionDetailsDTO edition1 = new EditionDetailsDTO()
+			.id(33L).name("Edition").course(new CourseSummaryDTO().id(8732L).code("EDED").name("Coursy"));
+
+	private final ModuleDetailsDTO module1 = new ModuleDetailsDTO()
+			.id(53L).name("Module 1").edition(new EditionSummaryDTO().id(33L));
+
+	@BeforeEach
+	void setUp() {
+		when(mCache.getOrThrow(eq(module1.getId()))).thenReturn(module1);
+		when(eCache.getOrThrow(eq(edition1.getId()))).thenReturn(edition1);
+
+		when(mApi.addModule(any())).thenReturn(Mono.just(666L));
+	}
+
+	@Test
+	@WithUserDetails("admin1")
+	void getModuleCreatePageLoads() throws Exception {
+		mvc.perform(get("/edition/{id}/modules/create", edition1.getId()))
+				.andExpect(status().isOk())
+				.andExpect(view().name("module/create"))
+				.andExpect(model().attributeExists("dto", "edition"));
+	}
+
+	@Test
+	@WithUserDetails("admin1")
+	void createModuleCreatesANewModule() throws Exception {
+		mvc.perform(post("/edition/{id}/modules/create", edition1.getId()).with(csrf())
+				.queryParam("editionId", "" + edition1.getId())
+				.queryParam("name", "Cool Module"))
+				.andExpect(redirectedUrl("/edition/33/modules"));
+
+		verify(mApi).addModule(eq(new ModuleCreateDTO()
+				.name("Cool Module")
+				.edition(new EditionIdDTO().id(edition1.getId()))));
+	}
+
+	@Test
+	@WithUserDetails("admin1")
+	void getModuleRemovePageWorks() throws Exception {
+		mvc.perform(get("/module/{moduleId}/remove", module1.getId()))
+				.andExpect(status().isOk())
+				.andExpect(view().name("module/remove"))
+				.andExpect(model().attributeExists("edition", "_module"));
+	}
+
+	@Test
+	@WithUserDetails("admin1")
+	void removeModuleRedirectsBackToModulesOverview() throws Exception {
+		mvc.perform(post("/module/{moduleId}/remove", module1.getId()).with(csrf()))
+				.andExpect(redirectedUrl("/edition/" + edition1.getId() + "/modules"));
+	}
+
+	@ParameterizedTest
+	@MethodSource(value = "protectedEndpoints")
+	void testWithoutUserDetailsIsForbidden(MockHttpServletRequestBuilder request) throws Exception {
+		mvc.perform(request.with(csrf()))
+				.andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("http://localhost/login"));
+	}
+
+	private static List<MockHttpServletRequestBuilder> protectedEndpoints() {
+		return List.of(
+				get("/edition/{editionId}/modules/create", 1),
+				post("/edition/{editionId}/modules/create", 1),
+				get("/module/{moduleId}/remove", 1),
+				post("/module/{moduleId}/remove", 1));
+	}
+}
diff --git a/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java b/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..55fbbcc6283c52aad6e60d55b2eb8c9cbb3d2b88
--- /dev/null
+++ b/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java
@@ -0,0 +1,46 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.dto.create;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import nl.tudelft.labracore.api.dto.AssignmentCreateDTO;
+
+import org.junit.jupiter.params.provider.Arguments;
+
+public class QueueAssignmentCreateDTOTest extends CreateTest<AssignmentCreateDTO, QueueAssignmentCreateDTO> {
+	@Override
+	protected QueueAssignmentCreateDTO filledDTO() {
+		return QueueAssignmentCreateDTO.builder()
+				.name("Oei")
+				.moduleId(999L)
+				.description("This is a simple yet complicated description")
+				.deadline(LocalDateTime.of(2020, 9, 9, 20, 9, 19))
+				.build();
+	}
+
+	@Override
+	public List<Arguments> getters() {
+		return List.of(
+				of(QueueAssignmentCreateDTO::getName, AssignmentCreateDTO::getName),
+				of(QueueAssignmentCreateDTO::getModuleId, acd -> acd.getModule().getId()),
+				of(QueueAssignmentCreateDTO::getDeadline, AssignmentCreateDTO::getDeadline),
+				of(QueueAssignmentCreateDTO::getDescription, acd -> acd.getDescription().getText()));
+	}
+}
diff --git a/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java b/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f54aebb5bc6f3647b68845e257498b8306d2a7c
--- /dev/null
+++ b/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java
@@ -0,0 +1,41 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.dto.create;
+
+import java.util.List;
+
+import nl.tudelft.labracore.api.dto.ModuleCreateDTO;
+
+import org.junit.jupiter.params.provider.Arguments;
+
+public class QueueModuleCreateDTOTest extends CreateTest<ModuleCreateDTO, QueueModuleCreateDTO> {
+	@Override
+	protected QueueModuleCreateDTO filledDTO() {
+		return QueueModuleCreateDTO.builder()
+				.name("This is a name for a module")
+				.editionId(78434L)
+				.build();
+	}
+
+	@Override
+	public List<Arguments> getters() {
+		return List.of(
+				of(QueueModuleCreateDTO::getName, ModuleCreateDTO::getName),
+				of(QueueModuleCreateDTO::getEditionId, mcd -> mcd.getEdition().getId()));
+	}
+}
diff --git a/src/test/java/test/BaseMockConfig.java b/src/test/java/test/BaseMockConfig.java
index 83b11f749ebd385168579e036708083d645095e5..fd493e19d6e21c5cb5d2f5275f8ae033b5e8766d 100644
--- a/src/test/java/test/BaseMockConfig.java
+++ b/src/test/java/test/BaseMockConfig.java
@@ -67,6 +67,9 @@ public class BaseMockConfig {
 	@MockBean
 	private EditionCollectionControllerApi editionCollectionControllerApi;
 
+	@MockBean
+	private ModuleControllerApi moduleControllerApi;
+
 	@MockBean
 	private SessionControllerApi sessionControllerApi;