diff --git a/bff/pom.xml b/bff/pom.xml
index cfd48dd..32b3a1d 100644
--- a/bff/pom.xml
+++ b/bff/pom.xml
@@ -32,6 +32,10 @@
org.springframework.boot
spring-boot-starter-oauth2-client
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java
new file mode 100644
index 0000000..ada8cde
--- /dev/null
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ForbiddenException.java
@@ -0,0 +1,33 @@
+package se.su.dsv.studentportalen.bff.exception;
+
+import org.springframework.http.HttpStatus;
+
+/**
+ * Exception for authorization failures.
+ * Thrown when the user is authenticated but not allowed to perform the action.
+ */
+public class ForbiddenException extends StudentportalenException {
+
+ public ForbiddenException(String message) {
+ super(message);
+ }
+
+ public ForbiddenException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ @Override
+ public String getType() {
+ return "studentportalen:forbidden";
+ }
+
+ @Override
+ public String getTitle() {
+ return "Access denied";
+ }
+
+ @Override
+ public HttpStatus getStatus() {
+ return HttpStatus.FORBIDDEN;
+ }
+}
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..f54ddd9
--- /dev/null
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/GlobalExceptionHandler.java
@@ -0,0 +1,96 @@
+package se.su.dsv.studentportalen.bff.exception;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ProblemDetail;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * Centralized exception handler for all REST controllers.
+ * Returns RFC 7807 Problem Details for all error responses.
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ private static final Logger LOG = LoggerFactory.getLogger(
+ GlobalExceptionHandler.class
+ );
+
+ /**
+ * Handles Jakarta Bean Validation errors (missing/invalid fields).
+ * Triggered when @Valid fails on request body.
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
+ Map> violations = ex
+ .getBindingResult()
+ .getFieldErrors()
+ .stream()
+ .collect(
+ Collectors.groupingBy(
+ FieldError::getField,
+ Collectors.mapping(
+ error ->
+ error.getDefaultMessage() != null
+ ? error.getDefaultMessage()
+ : "Invalid value",
+ Collectors.toList()
+ )
+ )
+ );
+
+ LOG.warn("Validation failed: {}", violations);
+
+ ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
+ problem.setType(URI.create("studentportalen:validation-error"));
+ problem.setTitle("Validation failed");
+ problem.setDetail("One or more fields are invalid");
+ problem.setProperty("violations", violations);
+ return problem;
+ }
+
+ /**
+ * Handles business exceptions from the BFF.
+ * These include mapped errors from backend systems like Daisy.
+ */
+ @ExceptionHandler(StudentportalenException.class)
+ public ProblemDetail handleBusinessError(StudentportalenException ex) {
+ LOG.warn("Business error: {} - {}", ex.getType(), ex.getMessage());
+
+ ProblemDetail problem = ProblemDetail.forStatus(ex.getStatus());
+ problem.setType(URI.create(ex.getType()));
+ problem.setTitle(ex.getTitle());
+ problem.setDetail(ex.getMessage());
+
+ if (ex instanceof ValidationException ve && !ve.getViolations().isEmpty()) {
+ problem.setProperty("violations", ve.getViolations());
+ }
+
+ return problem;
+ }
+
+ /**
+ * Handles unexpected errors.
+ * Logs the full stack trace but returns a generic message to the client.
+ */
+ @ExceptionHandler(Exception.class)
+ public ProblemDetail handleUnexpected(Exception ex) {
+ LOG.error("Unexpected error", ex);
+
+ ProblemDetail problem = ProblemDetail.forStatus(
+ HttpStatus.INTERNAL_SERVER_ERROR
+ );
+ problem.setType(URI.create("studentportalen:internal-error"));
+ problem.setTitle("An unexpected error occurred");
+ problem.setDetail("Please try again later");
+ return problem;
+ }
+}
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java
new file mode 100644
index 0000000..d160d1e
--- /dev/null
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ServiceUnavailableException.java
@@ -0,0 +1,33 @@
+package se.su.dsv.studentportalen.bff.exception;
+
+import org.springframework.http.HttpStatus;
+
+/**
+ * Exception for backend service unavailability.
+ * Thrown when a backend system (e.g., Daisy) is unreachable or returns 5xx errors.
+ */
+public class ServiceUnavailableException extends StudentportalenException {
+
+ public ServiceUnavailableException(String message) {
+ super(message);
+ }
+
+ public ServiceUnavailableException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ @Override
+ public String getType() {
+ return "studentportalen:service-unavailable";
+ }
+
+ @Override
+ public String getTitle() {
+ return "Service unavailable";
+ }
+
+ @Override
+ public HttpStatus getStatus() {
+ return HttpStatus.SERVICE_UNAVAILABLE;
+ }
+}
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java
new file mode 100644
index 0000000..193f2a0
--- /dev/null
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/StudentportalenException.java
@@ -0,0 +1,34 @@
+package se.su.dsv.studentportalen.bff.exception;
+
+import org.springframework.http.HttpStatus;
+
+/**
+ * Base exception for all Studentportalen BFF errors.
+ * Subclasses define specific error types that map to RFC 7807 Problem Details.
+ */
+public abstract class StudentportalenException extends RuntimeException {
+
+ protected StudentportalenException(String message) {
+ super(message);
+ }
+
+ protected StudentportalenException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * The error type URI for RFC 7807 Problem Details.
+ * Example: "studentportalen:validation-error"
+ */
+ public abstract String getType();
+
+ /**
+ * A short, human-readable summary of the problem type.
+ */
+ public abstract String getTitle();
+
+ /**
+ * The HTTP status code for this error.
+ */
+ public abstract HttpStatus getStatus();
+}
diff --git a/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java
new file mode 100644
index 0000000..1d69220
--- /dev/null
+++ b/bff/src/main/java/se/su/dsv/studentportalen/bff/exception/ValidationException.java
@@ -0,0 +1,49 @@
+package se.su.dsv.studentportalen.bff.exception;
+
+import java.util.List;
+import java.util.Map;
+import org.springframework.http.HttpStatus;
+
+/**
+ * Exception for validation errors, including field-level violations.
+ * Used for both BFF validation failures and mapped Daisy validation errors.
+ */
+public class ValidationException extends StudentportalenException {
+
+ private final Map> violations;
+
+ public ValidationException(String message) {
+ this(message, Map.of());
+ }
+
+ public ValidationException(
+ String message,
+ Map> violations
+ ) {
+ super(message);
+ this.violations = violations != null ? Map.copyOf(violations) : Map.of();
+ }
+
+ @Override
+ public String getType() {
+ return "studentportalen:validation-error";
+ }
+
+ @Override
+ public String getTitle() {
+ return "Validation failed";
+ }
+
+ @Override
+ public HttpStatus getStatus() {
+ return HttpStatus.BAD_REQUEST;
+ }
+
+ /**
+ * Field-level validation errors.
+ * Keys are field names, values are lists of error messages.
+ */
+ public Map> getViolations() {
+ return violations;
+ }
+}