diff --git a/include/ada.h b/include/ada.h
index c5d0946ec..04eacb168 100644
--- a/include/ada.h
+++ b/include/ada.h
@@ -9,6 +9,7 @@
 #include "ada/character_sets-inl.h"
 #include "ada/checkers-inl.h"
 #include "ada/common_defs.h"
+#include "ada/ada_data_url.h"
 #include "ada/log.h"
 #include "ada/encoding_type.h"
 #include "ada/helpers.h"
diff --git a/include/ada/ada_data_url.h b/include/ada/ada_data_url.h
new file mode 100644
index 000000000..e9fc456f5
--- /dev/null
+++ b/include/ada/ada_data_url.h
@@ -0,0 +1,33 @@
+#ifndef ADA_DATA_URL_H
+#define ADA_DATA_URL_H
+
+#include <string_view>
+
+namespace ada::data_url {
+// https://fetch.spec.whatwg.org/#data-url-struct
+struct data_url {
+    data_url() = default;
+    data_url(const data_url &m) = default;
+    data_url(data_url &&m) noexcept = default;
+    data_url &operator=(data_url &&m) noexcept = default;
+    data_url &operator=(const data_url &m) = default;
+    ~data_url() = default;
+
+    bool is_valid = true;
+    std::string body{};
+    std::string essence{};
+};
+
+ada::data_url::data_url parse_data_url(std::string_view data_url);
+
+std::string collect_sequence_of_code_points(char c, const std::string& input, size_t& position);
+
+bool is_ascii_whitespace(char c);
+
+std::string remove_ascii_whitespace(std::string input, bool leading, bool trailing);
+
+static constexpr bool is_base64(std::string_view input);
+
+}
+
+#endif  // ADA_DATA_URL_H
diff --git a/include/ada/serializers.h b/include/ada/serializers.h
index 260dcf3a1..e3801d8dc 100644
--- a/include/ada/serializers.h
+++ b/include/ada/serializers.h
@@ -39,6 +39,8 @@ std::string ipv6(const std::array<uint16_t, 8>& address) noexcept;
  */
 std::string ipv4(uint64_t address) noexcept;
 
+std::string url_serializer(const ada::url& url, bool excludeFragment) noexcept;
+
 }  // namespace ada::serializers
 
 #endif  // ADA_SERIALIZERS_H
diff --git a/src/ada.cpp b/src/ada.cpp
index 26090909f..a179e6819 100644
--- a/src/ada.cpp
+++ b/src/ada.cpp
@@ -9,3 +9,5 @@
 #include "url_components.cpp"
 #include "url_aggregator.cpp"
 #include "ada_c.cpp"
+#include "ada_data_url.cpp"
+
diff --git a/src/ada_data_url.cpp b/src/ada_data_url.cpp
new file mode 100644
index 000000000..a5c831c6f
--- /dev/null
+++ b/src/ada_data_url.cpp
@@ -0,0 +1,142 @@
+#include <string_view>
+#include <cctype>
+
+#include "ada.h"
+
+namespace ada::data_url {
+
+ada::data_url::data_url parse_data_url(std::string_view data_url) {
+  auto out = ada::data_url::data_url();
+
+  auto url = ada::parse<ada::url>(data_url, nullptr);
+
+  // 1. Assert: dataURL’s scheme is "data".
+  if (!url || url->get_protocol() != "data:") {
+      out.is_valid = false;
+      return out;
+  }
+
+  // 2. Let input be the result of running the URL serializer on dataURL with exclude
+  //    fragment set to true.
+  url->set_hash({});
+  auto input = url->get_href();
+
+  // 3. Remove the leading "data:" from input.
+  input.erase(0, 5);
+
+  // 4. Let position point at the start of input.
+  size_t position = 0;
+
+  // 5. Let mimeType be the result of collecting a sequence of code points that are
+  //    not equal to U+002C (,), given position.
+  auto mimetype = collect_sequence_of_code_points(',', input, position);
+  auto mimetype_length = mimetype.length();
+
+  // 6. Strip leading and trailing ASCII whitespace from mimeType.
+  mimetype = remove_ascii_whitespace(mimetype, true, true);
+
+  // 7. If position is past the end of input, then return failure.
+  if (position >= input.length()) {
+      out.is_valid = false;
+      return out;
+  }
+
+  // 8. Advance position by 1.
+  position++;
+
+  // 9. Let encodedBody be the remainder of input.
+  std::string encoded_body = input.substr(mimetype_length + 1);
+
+  // 10. Let body be the percent-decoding of encodedBody.
+  encoded_body = ada::unicode::percent_decode(encoded_body, encoded_body.find('%'));
+
+  // 11. If mimeType ends with U+003B (;), followed by zero or more U+0020 SPACE,
+  //     followed by an ASCII case-insensitive match for "base64", then:
+  size_t last_semi_colon = input.find_last_of(';');
+
+  if (last_semi_colon != std::string::npos) {
+    size_t next_non_space = input.find_first_not_of(' ', last_semi_colon);
+
+    out.essence = mimetype.substr(0, last_semi_colon);
+
+    if (is_base64(mimetype)) {
+
+        // 11.1. Let stringBody be the isomorphic decode of body.
+        auto string_body = encoded_body;
+
+        // 11.2. Set body to the forgiving-base64 decode of stringBody.
+        // 11.3. If body is failure, then return failure.
+        // TODO
+        out.body = string_body;
+
+        // 11.4. Remove the last 6 code points from mimeType.
+        // 11.5. Remove trailing U+0020 SPACE code points from mimeType, if any.
+        // 11.6. Remove the last U+003B (;) from mimeType.
+        mimetype.erase(last_semi_colon);
+    }
+  }
+
+  // 12. If mimeType starts with ";", then prepend "text/plain" to mimeType.
+  if (mimetype.starts_with(';')) {
+      mimetype = "text/plain" + mimetype;
+  }
+
+  return out;
+}
+
+std::string collect_sequence_of_code_points(char c, const std::string& input, size_t& position) {
+    auto idx = input.find_first_of(c, position);
+    size_t start = position;
+
+    if (idx == std::string::npos) {
+        position = static_cast<size_t>(input.length());
+        return input.substr(start);
+    }
+
+    position = static_cast<size_t>(idx);
+    return input.substr(start, position);
+}
+
+std::string remove_ascii_whitespace(std::string input, bool leading, bool trailing) {
+    size_t lead = 0;
+    size_t trail = input.length();
+
+    if (leading) {
+        while (lead < input.length() && is_ascii_whitespace(input[lead])) {
+            lead++;
+        }
+
+        if (lead != 0) {
+            input.erase(lead);
+        }
+    }
+
+    if (trailing) {
+        while (trail > 0 && is_ascii_whitespace(input[trail])) {
+            trail--;
+        }
+
+        if (trail != input.length()) {
+            input.resize(input.length() - trail);
+        }
+    }
+
+    return input;
+}
+
+bool is_ascii_whitespace(char c) {
+    return c == '\r' || c == '\n' || c == '\t' || c == '\f' || c == ' ';
+}
+
+static constexpr bool is_base64(std::string_view input) {
+    auto last_idx = input.find_last_of(';');
+    if (last_idx != std::string_view::npos) {
+        // TODO(@anonrig): Trim input
+        auto res = input.substr(last_idx + 1);
+        return res.size() == 6 && (res[0] | 0x20) == 'b' && (res[1] | 0x20) == 'a' &&
+               (res[2] | 0x20) == 's' && (res[3] | 0x20) == 'e' && (res[4] == '6') && (res[5] == '4');
+    }
+    return false;
+}
+
+}
diff --git a/src/serializers.cpp b/src/serializers.cpp
index 91be39ce1..cc9a9974b 100644
--- a/src/serializers.cpp
+++ b/src/serializers.cpp
@@ -77,4 +77,22 @@ std::string ipv4(const uint64_t address) noexcept {
   return output;
 }
 
+std::string url_serializer(const ada::url& url, bool excludeFragment) noexcept {
+    if (!excludeFragment) {
+        return url.get_href();
+    }
+
+    std::string href = url.get_href();
+    size_t hashLength = url.has_hash() ? url.get_hash().size() : 0;
+
+    std::string serialized = hashLength == 0 ? href : href.substr(0, href.length() - hashLength);
+
+    if (hashLength == 0 && href.ends_with('#')) {
+        serialized.pop_back();
+        return serialized;
+    }
+
+    return serialized;
+}
+
 }  // namespace ada::serializers
diff --git a/tests/basic_tests.cpp b/tests/basic_tests.cpp
index d1d452a42..be35463e1 100644
--- a/tests/basic_tests.cpp
+++ b/tests/basic_tests.cpp
@@ -462,4 +462,11 @@ TYPED_TEST(basic_tests, negativeport) {
   auto url = ada::parse<TypeParam>("https://www.google.com");
   ASSERT_FALSE(url->set_port("-1"));
   SUCCEED();
+}
+
+TYPED_TEST(basic_tests, data_url) {
+    auto data_url = ada::data_url::parse_data_url("data:application/octet-stream;base64,YWJj");
+    ASSERT_TRUE(data_url.is_valid);
+    ASSERT_EQ(data_url.essence, "application/octet-stream");
+    ASSERT_EQ(data_url.body, "YWJj");
 }
\ No newline at end of file