diff --git a/resources/assets/javascripts/bootstrap/consultations.js b/resources/assets/javascripts/bootstrap/consultations.js
index ef79d9ca1e6760be1c2c493c2add338636f3d895..51ffa8530859a373efa7ade3f23c81123a9233c7 100644
--- a/resources/assets/javascripts/bootstrap/consultations.js
+++ b/resources/assets/javascripts/bootstrap/consultations.js
@@ -10,9 +10,9 @@ $(document).on('click', '.consultation-delete-check:not(.ignore)', event => {
     }
 
     let requests = ids.map(id => {
-        return STUDIP.jsonapi.GET(`consultation-slots/${id}/bookings`).then(result => result.data.length);
+        return STUDIP.jsonapi.withPromises().get(`consultation-slots/${id}/bookings`).then(response => response.data.length);
     });
-    $.when(...requests).done((...results) => {
+    Promise.all(requests).then((...results) => {
         if (results.some(result => result > 0)) {
             $(event.target).addClass('ignore').click().removeClass('ignore');
         } else {
diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js
index 2d592be2ca452e85869394e4a1a26aed4730da90..fe20c9ae894c82b70f7dd15373f6c88d2cec6c48 100644
--- a/resources/assets/javascripts/init.js
+++ b/resources/assets/javascripts/init.js
@@ -38,7 +38,7 @@ import HeaderMagic from './lib/header_magic.js';
 import i18n from './lib/i18n.js';
 import Instschedule from './lib/instschedule.js';
 import InlineEditing from './lib/inline-editing.js';
-import JSONAPI, { jsonapi } from './lib/jsonapi.js';
+import JSONAPI, { jsonapi } from './lib/jsonapi.ts';
 import JSUpdater from './lib/jsupdater.js';
 import Lightbox from './lib/lightbox.js';
 import Markup from './lib/markup.js';
diff --git a/resources/assets/javascripts/lib/abstract-api.js b/resources/assets/javascripts/lib/abstract-api.js
index 70a88dbbd60217aa4cc7cba61fcd190283814f7e..95ae015c5f601bb9ebb3e67d5dcffe675397f324 100644
--- a/resources/assets/javascripts/lib/abstract-api.js
+++ b/resources/assets/javascripts/lib/abstract-api.js
@@ -1,5 +1,20 @@
 import Overlay from './overlay.js';
 
+class APIError extends Error
+{
+    static createWithJqXhr(message, jqXhr) {
+        const error = new APIError(message);
+        error.setJqXhr(jqXhr);
+        return error;
+    }
+
+    jqXhr = null;
+
+    setJqXhr(jqXhr) {
+        this.jqXhr = jqXhr;
+    }
+}
+
 class AbstractAPI
 {
     static get supportedMethods() {
@@ -118,6 +133,31 @@ class AbstractAPI
             }
         }).join('&');
     }
+
+    withPromises() {
+        return new Proxy(this, {
+            get(target, prop, receiver) {
+                // This will allow http methods to be written as lowercase when called as methods
+                // (e.g. api.patch() instead of api.PATCH())
+                if (target[prop] === undefined && AbstractAPI.supportedMethods.includes(prop.toUpperCase())) {
+                    prop = prop.toUpperCase();
+                }
+
+                // Only handle calls to request methods
+                if (prop !== 'request') {
+                    return Reflect.get(target, prop, receiver);
+                }
+
+                // Return a wrapped promise that handles the deferred
+                return (url, options = {}) => new Promise((resolve, reject) => {
+                    target[prop].apply(target, [url, options]).then(
+                        (response) => resolve(response),
+                        (jqXhr, textStatus, errorThrown) => reject(APIError.createWithJqXhr(errorThrown || textStatus, jqXhr))
+                    );
+                });
+            }
+        })
+    }
 }
 
 // Create shortcut methods for easier access by method
diff --git a/resources/assets/javascripts/lib/jsonapi.js b/resources/assets/javascripts/lib/jsonapi.ts
similarity index 75%
rename from resources/assets/javascripts/lib/jsonapi.js
rename to resources/assets/javascripts/lib/jsonapi.ts
index f3217bc6e6e50cd3ed17763600e364dc4333ad63..80176ccb6ca43091ff0477ed1f445b4d48b600e3 100644
--- a/resources/assets/javascripts/lib/jsonapi.js
+++ b/resources/assets/javascripts/lib/jsonapi.ts
@@ -3,11 +3,11 @@ import AbstractAPI from './abstract-api.js';
 // Actual JSONAPI object
 class JSONAPI extends AbstractAPI
 {
-    constructor(version = 1) {
+    constructor(version: number = 1) {
         super(`jsonapi.php/v${version}`);
     }
 
-    encodeData (data, method) {
+    encodeData (data: any, method: string): any {
         data = super.encodeData(data);
 
         if (['DELETE', 'GET', 'HEAD'].includes(method)) {
@@ -21,11 +21,11 @@ class JSONAPI extends AbstractAPI
         return JSON.stringify(data);
     }
 
-    request (url, options = {}) {
+    request (url: string, options: any = {}) {
         options.contentType = 'application/vnd.api+json';
         return super.request(url, options);
     }
 }
 
 export default JSONAPI;
-export const jsonapi = new JSONAPI();
+export const jsonapi: JSONAPI = new JSONAPI();
diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js
index f5812952a850cb886879d775bff288fc7fb8dca0..e611150f7b22113e6ec4e37aa5daeac2d9ff8d71 100644
--- a/resources/assets/javascripts/studip-ui.js
+++ b/resources/assets/javascripts/studip-ui.js
@@ -33,9 +33,9 @@ import eventBus from "./lib/event-bus.ts";
         if (STUDIP.UI.restrictedDates[year] === undefined) {
             STUDIP.UI.restrictedDates[year] = {};
 
-            STUDIP.jsonapi.GET('holidays', {data: {
+            STUDIP.jsonapi.withPromises().get('holidays', {data: {
                 'filter[year]': year
-            }}).done(response => {
+            }}).then(response => {
                 // Since PHP will return an empty object as an array,
                 // we need to check
                 if (Array.isArray(response)) {
diff --git a/resources/vue/components/MyCoursesTiles.vue b/resources/vue/components/MyCoursesTiles.vue
index 73f8aad308bc67f28922bab8a1d17cc97253603b..12b2627258ab00e501846ba0d2add16a44bd3fcd 100644
--- a/resources/vue/components/MyCoursesTiles.vue
+++ b/resources/vue/components/MyCoursesTiles.vue
@@ -136,7 +136,7 @@ export default {
             return this.shownColorPicker === course.id;
         },
         changeColor(course, index) {
-            STUDIP.jsonapi.PATCH(`course-memberships/${course.id}_${this.userid}`, {
+            STUDIP.jsonapi.withPromises().patch(`course-memberships/${course.id}_${this.userid}`, {
                 data: {
                     data: {
                         type: 'course-memberships',
@@ -145,9 +145,9 @@ export default {
                         }
                     }
                 }
-            }).done(() => {
+            }).then(() => {
                 course.group = index;
-            }).always(() => {
+            }).finally(() => {
                 this.shownColorPicker = null;
             });
         },
diff --git a/resources/vue/store/ContentModulesStore.js b/resources/vue/store/ContentModulesStore.js
index 9dc4609630f668a433eccaca598829529a84bc4a..141a1780dc6ec106f4da103293e69da30d71799b 100644
--- a/resources/vue/store/ContentModulesStore.js
+++ b/resources/vue/store/ContentModulesStore.js
@@ -52,7 +52,7 @@ export default {
                 attributes: { value: view === 'tiles' }
             };
 
-            return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } }) ;
+            return STUDIP.jsonapi.withPromises().patch(`config-values/${documentId}`, { data: { data } }) ;
         },
         exchangeModules({ commit, state }, modules) {
             const order = modules.filter(module => module.active)
diff --git a/resources/vue/store/MyCoursesStore.js b/resources/vue/store/MyCoursesStore.js
index 08c03891bef1afb3e1139092aa5b5675e79ef36d..af8e0fc464471f9d8540c6b63c34cc04db46ea8f 100644
--- a/resources/vue/store/MyCoursesStore.js
+++ b/resources/vue/store/MyCoursesStore.js
@@ -71,7 +71,7 @@ export default {
                 attributes: { value: configValue[configKey] }
             };
 
-            return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } })
+            return STUDIP.jsonapi.withPromises().patch(`config-values/${documentId}`, { data: { data } })
         },
         toggleOpenGroup ({ state, dispatch }, group) {
             let open_groups = [ ...state.config.open_groups ];