<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>S3 Object Storage Archives - Codersee blog- Kotlin on the backend</title>
	<atom:link href="https://blog.codersee.com/tag/s3-object-storage/feed/" rel="self" type="application/rss+xml" />
	<link></link>
	<description>Kotlin &#38; Backend Tutorials - Learn Through Practice.</description>
	<lastBuildDate>Wed, 16 Apr 2025 04:49:36 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://blog.codersee.com/wp-content/uploads/2025/04/cropped-codersee_logo_circle_2-32x32.png</url>
	<title>S3 Object Storage Archives - Codersee blog- Kotlin on the backend</title>
	<link></link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Test Spring Boot AWS S3 with Localstack and Testcontainers</title>
		<link>https://blog.codersee.com/test-spring-boot-aws-s3-with-localstack-and-testcontainers/</link>
					<comments>https://blog.codersee.com/test-spring-boot-aws-s3-with-localstack-and-testcontainers/#respond</comments>
		
		<dc:creator><![CDATA[Piotr]]></dc:creator>
		<pubDate>Tue, 24 Sep 2024 05:00:00 +0000</pubDate>
				<category><![CDATA[Spring]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[LocalStack]]></category>
		<category><![CDATA[S3 Object Storage]]></category>
		<category><![CDATA[Testcontainers]]></category>
		<category><![CDATA[Testing]]></category>
		<guid isPermaLink="false">https://codersee.com/?p=12009164</guid>

					<description><![CDATA[<p>The last article in a series dedicated to Spring Boot AWS S3 integration focused on testing with LocalStack and Testcontainers.</p>
<p>The post <a href="https://blog.codersee.com/test-spring-boot-aws-s3-with-localstack-and-testcontainers/">Test Spring Boot AWS S3 with Localstack and Testcontainers</a> appeared first on <a href="https://blog.codersee.com">Codersee blog- Kotlin on the backend</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Welcome to the <strong>last article</strong> in a series dedicated to integrating a Spring Boot Kotlin app with <strong>AWS S3</strong> Object Storage, in which we will focus on <strong>integration testing with LocalStack and Testcontainers</strong>. And although we will focus on Object Storage, the approach we will use can be easily replicated with other AWS services.</p>



<p>I can guarantee that you will benefit from this tutorial regardless of whether you saw previous articles about S3Client or S3Template, or not. But, I definitely encourage you to take a look at them, too: </p>



<ul class="wp-block-list">
<li><a href="https://blog.codersee.com/spring-boot-aws-s3-s3client-kotlin/">#1 Spring Boot with AWS S3, S3Client, and Kotlin</a></li>



<li><a href="https://blog.codersee.com/spring-boot-with-kotlin-aws-s3-and-s3template/">#2 Spring Boot with Kotlin, AWS S3, and S3Template</a></li>



<li>#3 (This article)</li>
</ul>



<h2 class="wp-block-heading" id="h-video-tutorial">Video Tutorial</h2>



<p>If you prefer <strong>video content</strong>, then check out my video that covers all three articles:</p>



<div>
<a href="https://blog.codersee.com/test-spring-boot-aws-s3-with-localstack-and-testcontainers/"><img decoding="async" src="https://blog.codersee.com/wp-content/plugins/wp-youtube-lyte/lyteCache.php?origThumbUrl=%2F%2Fi.ytimg.com%2Fvi%2FuTV9w1JehHM%2Fhqdefault.jpg" alt="YouTube Video"></a><br /><br /></p></div>



<p>If you find this content useful,<strong> please leave a subscription</strong> 🙂</p>



<h2 class="wp-block-heading" id="h-prerequisites">Prerequisites</h2>



<p>Before heading to the guide, I just wanted to emphasize that today we will be working with <strong>Testcontainers</strong>. And this means that we must have a <strong>supported Docker environment</strong>.</p>



<p>So, if you do not have Docker configured on your local and want to follow this article, please check out <a href="https://java.testcontainers.org/supported_docker_environment/" target="_blank" rel="noreferrer noopener">their documentation</a>.</p>



<p>Of course, you must have Java, IDE, and Spring Boot project too, but I believe this is quite obvious 😉 </p>



<h2 class="wp-block-heading" id="h-testcontainers-and-localstack">Testcontainers and LocalStack</h2>



<p>Lastly, I would like to say a few words about the Testcontainers and LocalStack, which in my opinion are a great way to test Spring Boot S3 integration (and other AWS integrations, too). </p>



<h3 class="wp-block-heading" id="h-testcontainers">Testcontainers</h3>



<p><strong>Testcontainers </strong>is a library for providing throwaway, lightweight instances of Docker containers. They are an excellent approach whenever need to test behavior dependent on external services, like AWS, or some external databases. </p>



<p>Long story short, instead of mocking, or manual set up of some test environment, we define test dependencies as code. Then, we can run our test code and disposable containers will be started and deleted after they finish. </p>



<p>Let&#8217;s take a look at the example from <a href="https://docs.spring.io/spring-boot/reference/testing/testcontainers.html" target="_blank" rel="noreferrer noopener">Spring Boot docs</a>: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@Testcontainers
@SpringBootTest
class MyIntegrationTests {

  @Test
  fun myTest() {
    // ...
  }

  companion object {
    @Container
    @JvmStatic
    val neo4j = Neo4jContainer("neo4j:5");
  }
}</pre>



<p>The above code runs a Neo4j docker container before the tests. Of course, this is just an example, so most of the time, we will need to add some more config. </p>



<p>Nevertheless, we can clearly see that this Testcontainers JUnit integration allows us to achieve our goal in an easy and neat manner. </p>



<h3 class="wp-block-heading" id="h-localstack">Localstack </h3>



<p><strong>Localstack</strong>, on the other hand, is a cloud service emulator that runs in a single container. In other words, we can run AWS applications or Lambdas <strong>without connecting to the remote cloud provider</strong>. </p>



<p>And thanks to the Testcontainers module for LocalStack, we can test various AWS integrations with just a few lines of code. </p>



<p>Again, let&#8217;s take a look at the example, but this time from the LocalStack documentation: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">DockerImageName localstackImage = DockerImageName.parse("localstack/localstack:3.5.0");

@Rule
public LocalStackContainer localstack = new LocalStackContainer(localstackImage)
        .withServices(S3);</pre>



<p>You will find links to both documentation at the end of this article. But for now, let&#8217;s not distract ourselves and focus on what we came here for 😉</p>



<h2 class="wp-block-heading" id="h-configure-project">Configure Project</h2>



<p>If you are following my S3 series, or you already have a Spring Boot project, then those are the necessary dependencies for us today: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">testImplementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.testcontainers:localstack")
testImplementation("org.springframework.boot:spring-boot-testcontainers")</pre>



<p>As we can see, apart from LocalStack and TestContainers, we must provide the Spring Boot Starter WebFlux. </p>



<p>But why? </p>



<p>Well, this is necessary to work with WebTestClient- a client we will use to test our web servers (REST endpoints). </p>



<p>On the other hand, if you would like to set up a project from scratch, then please navigate to the <a href="https://start.spring.io/" target="_blank" rel="noreferrer noopener">Spring Initializr</a> and select the following:</p>



<figure class="wp-block-image aligncenter size-large"><img fetchpriority="high" decoding="async" width="1024" height="517" src="http://blog.codersee.com/wp-content/uploads/2024/09/codersee_integration_testing_spring_boot_aws_s3_spring_initializr_page-1024x517.png" alt="Image is a screenshot from Spring Initializr page and shows the settings necessary for integartion testing of Spring Boot and AWS S3 connection." class="wp-image-12009173" srcset="https://blog.codersee.com/wp-content/uploads/2024/09/codersee_integration_testing_spring_boot_aws_s3_spring_initializr_page-1024x517.png 1024w, https://blog.codersee.com/wp-content/uploads/2024/09/codersee_integration_testing_spring_boot_aws_s3_spring_initializr_page-300x151.png 300w, https://blog.codersee.com/wp-content/uploads/2024/09/codersee_integration_testing_spring_boot_aws_s3_spring_initializr_page-768x388.png 768w, https://blog.codersee.com/wp-content/uploads/2024/09/codersee_integration_testing_spring_boot_aws_s3_spring_initializr_page.png 1507w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>However, please keep in mind that LocalStack is not provided out of the box in Spring, so we must add it manually. </p>



<p>Moreover, as we have chosen the Spring Web, the WebFlux dependency is not present, too:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">testImplementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.testcontainers:localstack")</pre>



<h2 class="wp-block-heading" id="h-testcontainers-singleton-approach">Testcontainers Singleton Approach</h2>



<p>With all of that being done, let&#8217;s head to the practice part. </p>



<p>When working with Testcontainers, we can configure them in various ways: </p>



<ul class="wp-block-list">
<li>we can use the<strong> JUnit extension</strong> (Jupiter integration)- which allows us to use <em>@Testcontainers</em> and <em>@Container</em> annotations and makes JUnit responsible for the automatic startup and stop of containers in our tests.</li>



<li>we can configure them <strong>manually </strong>in every test case, </li>



<li>or, alternatively, we can use the <strong>singleton approach</strong>&#8211; in which we control containers&#8217; lifecycle <a href="https://java.testcontainers.org/test_framework_integration/manual_lifecycle_control/" target="_blank" rel="noreferrer noopener">manually</a>. But, thanks to that we can easily reuse them across multiple test classes. </li>
</ul>



<p>Of course, these are not all approaches, and based on your needs you may want to configure Testcontainers differently. Nevertheless, in this tutorial, we will focus on the <strong>manual, reusable approach</strong>.</p>



<h3 class="wp-block-heading" id="h-introduce-base-class">Introduce Base Class</h3>



<p>Firstly, let&#8217;s introduce the <code>LocalStackIntegrationTest</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@SpringBootTest(webEnvironment = RANDOM_PORT)
class LocalStackIntegrationTest { }</pre>



<p>As we can see, we mark our class with <em>@SpringBootTest</em> &#8211; annotation necessary to run our integration tests and inject the instance of WebTestClient later in our subclasses. </p>



<h3 class="wp-block-heading" id="h-add-testcontainer">Add Testcontainer</h3>



<p>Following, let&#8217;s take a look at how to instantiate a LocalStack container:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">companion object {
  val localStack: LocalStackContainer = LocalStackContainer(
    DockerImageName.parse("localstack/localstack:3.7.2")
  )
}</pre>



<p>Right here, we create an instance of <strong>LocalStackContainer</strong> and we pass the name of a Docker image &#8211; <code>localstack/localstack:3.7.2</code>&#8211; to its constructor. Alternatively, if we are working only with AWS S3 Buckets service, then we can use a dedicated image- <code>localstack:s3-latest</code>. But personally, I am not a big fan of the <strong>latest</strong> tag, which can easily break our code.</p>



<p>Additionally, we put the LocalStackContainer instance in the <strong>companion object</strong>. Why? Because in the next steps, we will reference it in a function annotated with <em>@DynamicPropertySource</em>&#8211; and it must be static.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Note related to <strong>JUnit extension</strong>: </p>



<p><br>This is not the case here, as we want to take care of the container lifecycle manually, but, when using the Jupiter integration, containers declared as static fields will be shared between test methods. They will be started only once before any test method is executed and stopped after the last test method has executed. So, if in your case you pick the JUnit extension and don&#8217;t want that to happen, then you must not put the localstack in the companion object. </p>
</blockquote>



<h3 class="wp-block-heading" id="h-control-testcontainer-lifecycle">Control Testcontainer lifecycle</h3>



<p>As we already know, with this approach <strong>we are responsible</strong> for the container lifecycle control. And although this may sound complicated, it basically means that without the extension we must start the container manually.</p>



<p>So the companion object after the update will look, as follows:  </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">companion object {
  val localStack: LocalStackContainer = LocalStackContainer(
    DockerImageName.parse("localstack/localstack:3.7.2")
  ).apply {
    start()
  }
}</pre>



<p>Basically, we use the Kotlin scope function (you can learn more about it in my <a href="https://codersee.com/the-complete-kotlin-course/">Kotlin course</a>) to invoke the <code>start()</code> function on the <code>localStack</code> instance. And as the name suggests, this function will start the container (and pull the image, if necessary).</p>



<p>And basically,<strong> that is all we need to do here.</strong> With the above code, the container will be started when the base class is loaded and shared across all inheriting test classes. </p>



<p>Of course, there is also the <code>stop()</code> function that we can invoke to kill and remove the container. </p>



<p>Nevertheless, <strong>we do not have to do it</strong>. Why? Let&#8217;s figure out. </p>



<h3 class="wp-block-heading" id="h-ryuk">Ryuk</h3>



<p>Ryuk is a kind of &#8220;garbage collector&#8221; in Testcontainers. </p>



<p>Whenever we run integration tests, Testcontianers core starts <strong>one more container</strong>: </p>



<figure class="wp-block-image size-full"><img decoding="async" width="756" height="188" src="http://blog.codersee.com/wp-content/uploads/2024/09/image.png" alt="Image is a screenshot from Docker Desktop and presents two running container: the actual one and Ryuk started by Testcontainers core to clean up after integration testing is done." class="wp-image-12009191" srcset="https://blog.codersee.com/wp-content/uploads/2024/09/image.png 756w, https://blog.codersee.com/wp-content/uploads/2024/09/image-300x75.png 300w" sizes="(max-width: 756px) 100vw, 756px" /></figure>



<p>Long story short, this container is responsible for removing containers/networks/volumes created by our test cases. So, even if we do not clean the environment ourselves- for example with the <code>stop()</code> function- the Ryuk container will take care of that. </p>



<h3 class="wp-block-heading" id="h-test-properties-with-dynamicpropertysource">Test Properties With DynamicPropertySource</h3>



<p>With that done, we need to update our environment configuration. </p>



<p>If we try to run our Spring Boot application at this point, our logic responsible for communication with Amazon S3 will try to reach the <strong>actual AWS instance</strong>. It will use the defaults, or make use of the things we configured in the <code>application.yaml</code>. </p>



<p>And this is not what we want, right? Instead, we would like to connect to the Testcontainer LocalStack instance.  </p>



<p>In some examples, you might have seen the usage of application properties files. Nevertheless, if we want to be more flexible and make use of containers started on random ports, then the <strong>@DynamicPropertySource</strong> is our best friend here: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">companion object {
  val localStack: LocalStackContainer = LocalStackContainer(
    DockerImageName.parse("localstack/localstack:3.7.2")
  ).apply {
    start()
  }

  @JvmStatic
  @DynamicPropertySource
  fun overrideProperties(registry: DynamicPropertyRegistry) {
    registry.add("spring.cloud.aws.region.static") { localStack.region }
    registry.add("spring.cloud.aws.credentials.access-key") { localStack.accessKey }
    registry.add("spring.cloud.aws.credentials.secret-key") { localStack.secretKey }
    registry.add("spring.cloud.aws.s3.endpoint") { localStack.getEndpointOverride(S3).toString() }
  }

}</pre>



<p>Thanks to that annotation, we can dynamically provide values to our test environment <strong>based on the LocalStack instance</strong>.</p>



<p>Of course, we must remember that methods annotated with <em>@DynamicPropertySource</em> <strong>must be static</strong>! And that&#8217;s why we use it with the @JvmStatic annotation.</p>



<h2 class="wp-block-heading" id="h-utilize-localstack-aws-cli">Utilize LocalStack AWS CLI</h2>



<p>At this point, we have our base class ready, but before we head to the tests, I would like to show you the <strong>LocalStack AWS CLI</strong> and why and how to use it.  </p>



<p>As the first step, let&#8217;s create the <code>util</code> package and add the <code>LocalStackUtil.kt</code> file:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">import org.testcontainers.containers.localstack.LocalStackContainer

fun LocalStackContainer.createBucket(bucketName: String) {
  this.execInContainer("awslocal", "s3api", "create-bucket", "--bucket", bucketName)
}

fun LocalStackContainer.deleteBucket(bucketName: String) {
  this.execInContainer("awslocal", "s3api", "delete-bucket", "--bucket", bucketName)
}

fun LocalStackContainer.deleteObject(bucketName: String, objectName: String) {
  this.execInContainer("awslocal", "s3api", "delete-object", "--bucket", bucketName, "--key", objectName)
}</pre>



<p>As we can see, we introduced 3 helper extension functions that we will later use to create and delete buckets and objects. This way, we can <strong>easily clean up</strong> between the tests (we use the shared approach, right?). Moreover, it will <strong>simplify the setup process</strong> for each test case.</p>



<p>The above code combines the <code>execInContainer</code> &#8211; which will run the passed command in our running LocalStack container, just like with the <code>docker exec</code>, and the <code>awslocal</code>&#8211; a LocalStack wrapper around the AWS CLI. So, if you&#8217;ve ever been working with the AWS command line interface, then you will see that this is 1:1. </p>



<p>Unfortunately, we must provide the command as a separate String value, because otherwise, we will end up with: </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: &#8220;awslocal s3api create-bucket &#8211;bucket bucket-1&#8221;: executable file not found in $PATH: unknown</p>
</blockquote>



<h2 class="wp-block-heading" id="h-write-integration-test-cases">Write Integration Test Cases</h2>



<p>With all of that LocalStack preparation done (I know, quite a bunch of things to learn, but once you learn this, it will be a simple copy-paste), we can finally write some integration tests for our Spring Boot S3 integration.</p>



<p>Firstly, let&#8217;s create the <code>controller</code> package and put the <code>BucketControllerIntegrationTest</code> class:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">class BucketControllerIntegrationTest(
  @Autowired private val webTestClient: WebTestClient
) : LocalStackIntegrationTest() { }</pre>



<p>As we can see, no annotations are required. We simply extend the <code>LocalStackIntegrationTest</code> class and inject the <code>WebTestClient</code>.</p>



<h3 class="wp-block-heading" id="h-test-no-buckets-exist">Test No Buckets Exist</h3>



<p>Nextly, let&#8217;s introduce our first test case. If we do not do anything, we expect that no buckets exist in our S3 instance: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@Test
fun `Given no existing buckets When getting list of buckets Then return an empty array`() {
  val buckets = webTestClient
    .get().uri("/buckets")
    .exchange()
    .expectStatus().isOk()
    .expectBody(object : ParameterizedTypeReference&lt;List&lt;String>>() {})
    .returnResult()
    .responseBody

  assertNotNull(buckets)
  assertTrue(buckets.isEmpty())
}</pre>



<p>As mentioned before, we use the <em>WebTestClient</em> to make a GET HTTP request to the <code>/buckets</code> endpoint. Then, we use a small hack with <code>ParameterizedTypeReference</code>&#8211; because the endpoint returns a list of Strings and we use Kotlin- and we obtain the response body. </p>



<p>Lastly, we have plain assertions. We verify that the response body is not null and that our S3 bucket list is empty.</p>



<h3 class="wp-block-heading" id="h-verify-s3-bucket-exists-in-localstack">Verify S3 Bucket Exists In LocalStack</h3>



<p>Following, let&#8217;s see our helper functions in action:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@Test
fun `Given one existing bucket When getting list of buckets Then return an array with expected bucket name`() {
  val bucketName = "bucket-1"
  localStack.createBucket(bucketName)

  val expectedJson = """
    [ "Bucket #1: $bucketName" ]
  """

  webTestClient
    .get().uri("/buckets")
    .exchange()
    .expectStatus().isOk()
    .expectBody()
    .json(expectedJson)

  localStack.deleteBucket(bucketName)
}</pre>



<p>This time, we utilize the <code>createBucket</code> and make sure that the <code>/buckets</code> endpoint returns the expected JSON. Please note that this is another way to assert the response body.   </p>



<p>After all, we delete the existing bucket, so it won&#8217;t affect other test cases.</p>



<h3 class="wp-block-heading" id="h-assert-bucket-created-successfully">Assert Bucket Created Successfully</h3>



<p>As the next step, let&#8217;s take a look at how we can check if our endpoint responsible for creating new S3 buckets works. And I see two paths we can go here.</p>



<p>The first one, using the <code>execInContainer</code>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@Test
fun `Given no existing buckets When creating bucket Then create bucket successfully`() {
  val bucketName = "bucket-2"
  
  webTestClient
    .post().uri("/buckets")
    .bodyValue(BucketRequest(bucketName = bucketName))
    .exchange()
    .expectStatus().isOk()
  
  val execResult = localStack.execInContainer("awslocal", "s3api", "list-buckets").stdout
  
  assertTrue(execResult.contains(bucketName))  
  localStack.deleteBucket(bucketName)
}</pre>



<p>The important thing to mention here is that the <code>execInContainer</code> returns the <code>ExecResult</code>. And thanks to that, we can read additional info, like <code>stdout</code>, <code>stderr</code>, or <code>exitCode</code>.</p>



<p>And thanks to the <code>stdout</code>, we can get this JSON to verify it contains particular bucket name (or even we could parse that to an object): </p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "Buckets": [
    {
      "Name": "bucket-2",
      "CreationDate": "2024-09-19T05:28:42.000Z"
    }
  ],
  "Owner": {
    "DisplayName": "webfile",
    "ID": "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a"
  }
}</pre>



<p>Alternatively, we can use the <code>/buckets</code> endpoint once again, too:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@Test
fun `Given no existing buckets When creating bucket Then create bucket successfully`() {
  val bucketName = "bucket-2"

  webTestClient
    .post().uri("/buckets")
    .bodyValue(BucketRequest(bucketName = bucketName))
    .exchange()
    .expectStatus().isOk()

  val expectedJson = """
    [ "Bucket #1: $bucketName" ]
  """
  webTestClient
    .get().uri("/buckets")
    .exchange()
    .expectStatus().isOk()
    .expectBody()
    .json(expectedJson)

  localStack.deleteBucket(bucketName)
}</pre>



<h3 class="wp-block-heading" id="h-test-remaining-cases">Test Remaining Cases</h3>



<p>The remaining cases of our integration test use a more or less similar approach, so I will simply copy-paste them here so that you can analyze them. </p>



<p>At this point, I am pretty sure you understand the general idea behind what I understand by testing of Spring Boot S3 integration with LocalStack, so I don&#8217;t see the need for explaining them one- by one:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@Test
fun `Given no objects existing in the bucket When getting objects of a bucket Then return an empty array`() {
  val bucketName = "bucket-3"
  localStack.createBucket(bucketName)

  val objects = webTestClient
    .get().uri("/buckets/$bucketName/objects")
    .exchange()
    .expectStatus().isOk()
    .expectBody(object : ParameterizedTypeReference&lt;List&lt;String>>() {})
    .returnResult()
    .responseBody

  assertNotNull(objects)
  assertTrue(objects.isEmpty())

  localStack.deleteBucket(bucketName)
}

@Test
fun `Given no objects When creating example object Then return created object`() {
  val bucketName = "bucket-4"
  val objectName = "example.json"
  localStack.createBucket(bucketName)

  val expectedJson = """
    {
      "id": "123",
      "name": "Some name"
    }
  """

  webTestClient
    .post().uri("/buckets/$bucketName/objects")
    .exchange()
    .expectStatus().isOk()
    .expectBody()
    .json(expectedJson)

  localStack.deleteObject(bucketName, objectName)
  localStack.deleteBucket(bucketName)
}

@Test
fun `Given created object When getting list of objects Then return array with one object`() {
  val bucketName = "bucket-5"
  val objectName = "example.json"
  localStack.createBucket(bucketName)

  val expectedJson = """
    [ $objectName ]
  """

  webTestClient
    .post().uri("/buckets/$bucketName/objects")
    .exchange()
    .expectStatus().isOk()

  webTestClient
    .get().uri("/buckets/$bucketName/objects")
    .exchange()
    .expectStatus().isOk()
    .expectBody()
    .json(expectedJson)

  localStack.deleteObject(bucketName, objectName)
  localStack.deleteBucket(bucketName)
}

@Test
fun `Given existing object When getting object by key Then return object content`() {
  val bucketName = "bucket-6"
  val objectName = "example.json"
  localStack.createBucket(bucketName)

  val expected = """
    {
      "id": "123",
      "name": "Some name"
    }
  """

  webTestClient
    .post().uri("/buckets/$bucketName/objects")
    .exchange()

  webTestClient
    .get().uri("/buckets/$bucketName/objects/$objectName")
    .exchange()
    .expectStatus().isOk()
    .expectBody()
    .json(expected)

  localStack.deleteObject(bucketName, objectName)
  localStack.deleteBucket(bucketName)
}

@Test
fun `Given existing bucket with object When deleting bucket Then bucket is removed`() {
  val bucketName = "bucket-7"
  localStack.createBucket(bucketName)

  webTestClient
    .post().uri("/buckets/$bucketName/objects")
    .exchange()
    .expectStatus().isOk()

  webTestClient
    .delete().uri("/buckets/$bucketName")
    .exchange()
    .expectStatus().isOk()

  val buckets = webTestClient
    .get().uri("/buckets")
    .exchange()
    .expectStatus().isOk()
    .expectBody(object : ParameterizedTypeReference&lt;List&lt;String>>() {})
    .returnResult()
    .responseBody

  assertNotNull(buckets)
  assertTrue(buckets.isEmpty())
}</pre>



<h2 class="wp-block-heading" id="h-summary">Summary</h2>



<p>And that is all for this tutorial, in which we learned how to implement <strong>integration tests </strong>for <strong>Spring Boot AWS S3 </strong>integration with <strong>LocalStack </strong>and <strong>Testcontainers.</strong></p>



<p>I hope you enjoyed it and for the source code, please visit <a href="https://github.com/codersee-blog/spring-boot-3-kotlin-aws-s3-localstack-testcontainers" target="_blank" rel="noreferrer noopener">this GitHub repository</a>. </p>



<p>Have a great day and see you in the next articles! 🙂 </p>
<p>The post <a href="https://blog.codersee.com/test-spring-boot-aws-s3-with-localstack-and-testcontainers/">Test Spring Boot AWS S3 with Localstack and Testcontainers</a> appeared first on <a href="https://blog.codersee.com">Codersee blog- Kotlin on the backend</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.codersee.com/test-spring-boot-aws-s3-with-localstack-and-testcontainers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Spring Boot with Kotlin, AWS S3, and S3Template</title>
		<link>https://blog.codersee.com/spring-boot-with-kotlin-aws-s3-and-s3template/</link>
					<comments>https://blog.codersee.com/spring-boot-with-kotlin-aws-s3-and-s3template/#respond</comments>
		
		<dc:creator><![CDATA[Piotr]]></dc:creator>
		<pubDate>Tue, 17 Sep 2024 05:00:00 +0000</pubDate>
				<category><![CDATA[Spring]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[S3 Object Storage]]></category>
		<category><![CDATA[S3Template]]></category>
		<category><![CDATA[Spring Boot]]></category>
		<guid isPermaLink="false">https://codersee.com/?p=12009151</guid>

					<description><![CDATA[<p>The second article in a series dedicated to Spring Boot AWS S3 integration focused on S3Template and Kotlin.</p>
<p>The post <a href="https://blog.codersee.com/spring-boot-with-kotlin-aws-s3-and-s3template/">Spring Boot with Kotlin, AWS S3, and S3Template</a> appeared first on <a href="https://blog.codersee.com">Codersee blog- Kotlin on the backend</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Welcome to the <strong>second article</strong> in a series dedicated to integrating a Spring Boot Kotlin app with <strong>AWS S3 </strong>Object Storage, in which we will learn how to make our lives easier with <strong>S3Template</strong>. </p>



<p>Of course, I highly encourage you to take a look at other articles in this series, too: </p>



<ul class="wp-block-list">
<li><a href="https://blog.codersee.com/spring-boot-aws-s3-s3client-kotlin/">#1 Spring Boot with AWS S3, S3Client, and Kotlin</a></li>



<li>#2 (This article)</li>



<li><a href="https://blog.codersee.com/test-spring-boot-aws-s3-with-localstack-and-testcontainers/">#3 Test Spring Boot AWS S3 with Localstack and Testcontainers</a></li>
</ul>



<h2 class="wp-block-heading" id="h-video-tutorial">Video Tutorial</h2>



<p>If you prefer <strong>video content</strong>, then check out my video that covers all three articles:</p>



<div style="text-align: center; width: 90%; margin-left: 5%;">
<a href="https://blog.codersee.com/spring-boot-with-kotlin-aws-s3-and-s3template/"><img decoding="async" src="https://blog.codersee.com/wp-content/plugins/wp-youtube-lyte/lyteCache.php?origThumbUrl=%2F%2Fi.ytimg.com%2Fvi%2FuTV9w1JehHM%2Fhqdefault.jpg" alt="YouTube Video"></a><br /><br /></p></div>



<p>If you find this content useful,<strong> please leave a subscription</strong> 🙂</p>



<h2 class="wp-block-heading" id="h-what-is-s3template">What is S3Template? </h2>



<p>Before we get our hands dirty with the code, let&#8217;s take a second to understand better with S3Template is and how it can make our lives easier when integrating a Spring Boot app with S3. </p>



<p>Let me quote the S3Template documentation here: </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Higher level abstraction over S3Client providing methods for the most common use cases.</p>



<p>So, if you have already worked with, or you have seen my previous article with S3Client, then you saw that simple operations require some boilerplate. And that is exactly what S3Template solves. </p>



<p>And as a note from my end, I just wanted to note S3Template handles only some subset of S3Client operations, so in our projects those two will rather coexist, instead of being each others alternatives. </p>
</blockquote>



<h2 class="wp-block-heading" id="h-aws-s3template-operations">AWS S3Template Operations</h2>



<p>With all of that said, let&#8217;s get to work. </p>



<p>Let&#8217;s add the <code>controller</code> package and <code>BucketController</code> class to it. We will use it to expose a bunch of endpoints triggering various operations on S3 buckets and files. </p>



<p>When it comes to the operations- we will use the same ones as in the previous article, and as the last one, I will show you how to <strong>serialize and deserialize objects with AWS S3 and S3Template.</strong></p>



<h3 class="wp-block-heading" id="h-list-all-buckets">List All Buckets</h3>



<p>Although the S3Template does not expose any method that would help us with this task, I wanted to mention it as we discussed it in the previous tutorial. </p>



<p>Additionally, this is a great example of <code>S3Client</code> and <code>S3Template</code> co-existence: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@RestController
@RequestMapping("/buckets")
class BucketController(
  private val s3Template: S3Template,
  private val s3Client: S3Client,
) {

  @GetMapping
  fun listBuckets(): List&lt;String> {
    val response = s3Client.listBuckets()

    return response.buckets()
      .mapIndexed { index, bucket ->
        "Bucket #${index + 1}: ${bucket.name()}"
      }
  }
}</pre>



<p>As we can see, not too much S3Template could improve here, so I bet this is the reason why it was not introduced. </p>



<h3 class="wp-block-heading" id="h-new-s3-bucket">New S3 Bucket</h3>



<p>Nextly, let&#8217;s take a look at how we can create a brand new bucket: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@PostMapping
fun createBucket(@RequestBody request: BucketRequest) {
  s3Template.createBucket(request.bucketName)
}

data class BucketRequest(val bucketName: String)</pre>



<p>As we can see, no additional request classes- the only thing we need is the bucket name. </p>



<p>Of course, let&#8217;s rerun our application and verify if everything is working: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request POST 'http://localhost:8080/buckets' \
--header 'Content-Type: application/json' \
--data-raw '{
    "bucketName": "codersee-awesome-bucket"
}'</pre>



<p>As a result, we should get 200 OK without a response body. </p>



<h3 class="wp-block-heading" id="h-upload-file-to-s3-bucket">Upload File to S3 Bucket</h3>



<p>Nextly, let&#8217;s take a look at how to upload a new file to S3. </p>



<p>We have a few options, among which the <code>store</code> function is the easiest: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@PostMapping("/{bucketName}/objects")
fun createObject(@PathVariable bucketName: String, @RequestBody request: ObjectRequest) {
  s3Template.store(bucketName, request.objectName, request.content)
}

data class ObjectRequest(val objectName: String, val content: String)</pre>



<p>As we can see, this function takes three arguments: </p>



<ul class="wp-block-list">
<li>the bucket name, </li>



<li>filename,</li>



<li>and the content to upload (to be specific <code>Object object</code>) </li>
</ul>



<p>Alternatively, we could use the <code>upload</code> function, which allows us to send <code>InputStream</code> instance and metadata: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="java" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@Override
public S3Resource upload(
  String bucketName, 
  String key, 
  InputStream inputStream,
  @Nullable ObjectMetadata objectMetadata
) </pre>



<p>Lastly, let&#8217;s verify that everything is fine with the following curl: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request POST 'http://localhost:8080/buckets/codersee-awesome-bucket/objects' \
--header 'Content-Type: application/json' \
--data-raw '{
    "objectName": "file-example.txt",
    "content": "My file content"
}'</pre>



<p>If everything worked, then a new file should be present in our bucket 🙂 </p>



<h3 class="wp-block-heading" id="h-list-files-from-the-bucket">List Files From The Bucket</h3>



<p>Nextly, let&#8217;s see how we can list the bucket content with <em>S3Template</em>: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@GetMapping("/{bucketName}/objects")
fun listObjects(@PathVariable bucketName: String): List&lt;String> =
  s3Template.listObjects(bucketName, "")
    .map { s3Resource -> s3Resource.filename }</pre>



<p>We can clearly see that this is not rocket science 😉 </p>



<p>Nevertheless, it is worth mentioning that this time we get the <code>S3Resource</code> instance and instead of the <code>key()</code> we use it <code>getFilename</code> method. </p>



<p>And just like previously, let&#8217;s see the endpoint in action: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request POST 'http://localhost:8080/buckets/codersee-awesome-bucket/objects' \
--header 'Content-Type: application/json' \
--data-raw '{
    "objectName": "file-example.txt",
    "content": "My file content"
}'

# Response status: 200 OK
# Response body: 
[
    "file-example.txt"
]</pre>



<h3 class="wp-block-heading" id="h-download-a-file">Download a File</h3>



<p>So what about fetching files from the S3 bucket? </p>



<p>With <em>S3Template</em>, it&#8217;s a piece of cake: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@GetMapping("/{bucketName}/objects/{objectName}")
fun getObject(@PathVariable bucketName: String, @PathVariable objectName: String): String =
  s3Template.download(bucketName, objectName).getContentAsString(UTF_8)</pre>



<p>We specify the bucket name and the object name, and as a result, we get the <code>S3Resource</code> that exposes a bunch of methods. Among others, the <code>getContentAsString</code> that is pretty descriptive 😉 </p>



<p>Similarly, let&#8217;s hit the endpoint: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request GET 'http://localhost:8080/buckets/codersee-awesome-bucket/objects/file-example.txt'

# Response status: 200 OK
# Response body: 
"My file content"</pre>



<h3 class="wp-block-heading" id="h-delete-the-bucket">Delete the Bucket</h3>



<p>Last before least, let&#8217;s take a look at how we can get rid of the bucket: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@DeleteMapping("/{bucketName}")
fun deleteBucket(@PathVariable bucketName: String) {
  s3Template.listObjects(bucketName, "")
    .forEach { s3Template.deleteObject(bucketName, it.filename) }

  s3Template.deleteBucket(bucketName)
}</pre>



<p>And just like in the previous article- we must ensure the bucket does not contain any objects. </p>



<p>To do so, we list out objects by specifying the bucket name and objects prefix (as we don&#8217;t have any, we pass an empty String). Then, for each object, we use its key to delete it. And lastly, we simply invoke the <code>deleteBucket</code> by passing the name of a bucket to delete. </p>



<p>Of course, let&#8217;s verify this logic, too: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request DELETE 'http://localhost:8080/buckets/codersee-awesome-bucket'</pre>



<p>If we run this command and the S3 bucket exists, then we should see 200 OK and our bucket will disappear. </p>



<h3 class="wp-block-heading" id="h-serialize-deserialize-objects">Serialize/Deserialize objects </h3>



<p>As the last thing, let&#8217;s take a look at how easily we can persist objects using the combination of <code>store</code> and <code>read</code> functions: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@PostMapping("/{bucketName}/objects")
fun createExampleObject(@PathVariable bucketName: String): Example {
  val example = Example(id = UUID.randomUUID(), name = "Some name")

  s3Template.store(bucketName, "example.json", example)

  return s3Template.read(bucketName, "example.json", Example::class.java)
}

data class Example(val id: UUID, val name: String)</pre>



<p>As we can see, we made a small update to our <code>POST /{bucketName}/objects</code> endpoint logic. </p>



<p>Basically, the first part is exactly the same, we use the <code>store</code> again to push the file to the bucket. </p>



<p>Nevertheless, instead of the <code>download</code> we saw previously, we use the <code>read</code> function that uses the <code>S3ObjectConverter</code> that will automatically deserialize the JSON into the <code>Example</code> class instance. </p>



<p>And for the last time, let&#8217;s hit our API: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request POST 'http://localhost:8080/buckets/codersee-awesome-bucket/objects' \
--data-raw ''

# Response status: 200 OK 
# Response body: 
{
    "id": "3eacd8a3-48b2-4756-86db-e4c9f4e291da",
    "name": "Some name"
}</pre>



<p>And as we can see, the output confirms that everything is working fine. </p>



<p>Summary</p>



<p>And that&#8217;s all for this article on how to make our lives easier in Spring Boot with S3Template. </p>



<p>I hope you enjoyed it, and again wanted to invite you to take a look at other content of this series: </p>



<ul class="wp-block-list">
<li><a href="https://blog.codersee.com/spring-boot-aws-s3-s3client-kotlin/">#1 Spring Boot with AWS S3, S3Client, and Kotlin</a></li>



<li>#2 (This article)</li>



<li><a href="https://blog.codersee.com/test-spring-boot-aws-s3-with-localstack-and-testcontainers/">#3 Test Spring Boot AWS S3 with Localstack and Testcontainers</a></li>
</ul>



<p>Lastly, just wanted to show that you can find the source code in <a href="https://github.com/codersee-blog/spring-boot-3-kotlin-s3template" target="_blank" rel="noreferrer noopener">this GitHub repository</a> and that you can join my <a href="https://codersee.com/newsletter/">newsletter</a> to stay up-to-date with Kotlin on the backend. </p>
<p>The post <a href="https://blog.codersee.com/spring-boot-with-kotlin-aws-s3-and-s3template/">Spring Boot with Kotlin, AWS S3, and S3Template</a> appeared first on <a href="https://blog.codersee.com">Codersee blog- Kotlin on the backend</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.codersee.com/spring-boot-with-kotlin-aws-s3-and-s3template/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Spring Boot with AWS S3, S3Client and Kotlin</title>
		<link>https://blog.codersee.com/spring-boot-aws-s3-s3client-kotlin/</link>
					<comments>https://blog.codersee.com/spring-boot-aws-s3-s3client-kotlin/#comments</comments>
		
		<dc:creator><![CDATA[Piotr]]></dc:creator>
		<pubDate>Tue, 10 Sep 2024 06:00:00 +0000</pubDate>
				<category><![CDATA[Spring]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[S3 Object Storage]]></category>
		<category><![CDATA[S3Client]]></category>
		<guid isPermaLink="false">https://codersee.com/?p=12009125</guid>

					<description><![CDATA[<p>A step-by-step guide on how to configure a Spring Boot Kotlin app to work with AWS S3 Object Storage using the S3Client. </p>
<p>The post <a href="https://blog.codersee.com/spring-boot-aws-s3-s3client-kotlin/">Spring Boot with AWS S3, S3Client and Kotlin</a> appeared first on <a href="https://blog.codersee.com">Codersee blog- Kotlin on the backend</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Hello and welcome to the first article in a series dedicated to integrating a Spring Boot Kotlin app with <strong>AWS S3 Object Storage</strong>, in which I will show you how to <strong>properly set up the connection</strong> and <strong>make use of the S3Client</strong>. </p>



<p>What can you expect today? </p>



<p>Well, at the end of this tutorial, you will know precisely how to connect to the S3 service, resolve the most common issues related to that, as well as how to perform basic operations on <strong>buckets </strong>and <strong>objects </strong>using the <strong>S3Client approach</strong>. </p>



<p>In the future content, we will work a bit with an asynchronous client, learn when to use S3Template instead, and learn how to write tests properly, so do not hesitate to subscribe to my <a href="https://codersee.com/newsletter/">newsletter</a> 😉</p>



<h2 class="wp-block-heading" id="h-video-tutorial">Video Tutorial</h2>



<p>If you prefer <strong>video content</strong>, then check out my video that covers all three articles:</p>



<div style="text-align: center; width: 90%; margin-left: 5%;">
<a href="https://blog.codersee.com/spring-boot-aws-s3-s3client-kotlin/"><img decoding="async" src="https://blog.codersee.com/wp-content/plugins/wp-youtube-lyte/lyteCache.php?origThumbUrl=%2F%2Fi.ytimg.com%2Fvi%2FuTV9w1JehHM%2Fhqdefault.jpg" alt="YouTube Video"></a><br /><br /></p></div>



<p>If you find this content useful,<strong> please leave a subscription</strong> 🙂</p>



<h2 class="wp-block-heading" id="h-project-setup">Project Setup </h2>



<p>If you already have your Spring Boot project prepared, then feel free to skip this step. </p>



<p>But if that is not the case, then let&#8217;s navigate together to the <a href="https://start.spring.io/" target="_blank" rel="noreferrer noopener">Spring Initializr</a> page and select the following settings: </p>



<figure class="wp-block-image aligncenter size-large"><img decoding="async" width="1024" height="534" src="http://blog.codersee.com/wp-content/uploads/2024/09/spring_initializr_page-1024x534.png" alt="" class="wp-image-12009136" srcset="https://blog.codersee.com/wp-content/uploads/2024/09/spring_initializr_page-1024x534.png 1024w, https://blog.codersee.com/wp-content/uploads/2024/09/spring_initializr_page-300x156.png 300w, https://blog.codersee.com/wp-content/uploads/2024/09/spring_initializr_page-768x400.png 768w, https://blog.codersee.com/wp-content/uploads/2024/09/spring_initializr_page.png 1510w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>As we can see, nothing related to the AWS S3 yet, but we imported the <code>Spring Web</code> dependency so that we could expose REST endpoints to test. </p>



<p>As always, please generate the project, extract it on your local, and import it to your favorite IDE. </p>



<h2 class="wp-block-heading" id="h-aws-s3-dependencies">AWS S3 Dependencies</h2>



<p>It is worth mentioning that the <strong>S3Client</strong> is not a Spring Boot concept, but a class that comes from the AWS SDK. </p>



<p><strong>However</strong>, to make our lives easier when working with Spring, we can make use of the Spring Cloud AWS, which simplifies using AWS-managed services in a Spring and Spring Boot applications.</p>



<p>To do so, let&#8217;s navigate to the <code>build.gradle.kts</code> and add the following: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">implementation("io.awspring.cloud:spring-cloud-aws-starter-s3:3.1.1")</pre>



<p>Why do I pick it over the AWS Java SDK? </p>



<p>Because it auto-configures various S3 integration-related components out of the box. Additionally, we can quickly configure a bunch of things using the <code>application.yaml</code>. And lastly, we are all Spring boyzz here, we don&#8217;t do things manually 🙂</p>



<h2 class="wp-block-heading" id="h-create-test-s3-bucket">Create Test S3 Bucket</h2>



<p>Following, let&#8217;s navigate to the <a href="https://console.aws.amazon.com/" target="_blank" rel="noreferrer noopener">AWS Console</a> to prepare a test bucket. </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Again, feel free to skip it if you are looking for Spring Boot details.</p>
</blockquote>



<p>Then, let&#8217;s find the S3 (aka &#8220;Simple Storage Service&#8221;) in the search bar: </p>



<figure class="wp-block-image aligncenter size-full"><img loading="lazy" decoding="async" width="1008" height="538" src="http://blog.codersee.com/wp-content/uploads/2024/09/1.png" alt="" class="wp-image-12009138" srcset="https://blog.codersee.com/wp-content/uploads/2024/09/1.png 1008w, https://blog.codersee.com/wp-content/uploads/2024/09/1-300x160.png 300w, https://blog.codersee.com/wp-content/uploads/2024/09/1-768x410.png 768w" sizes="auto, (max-width: 1008px) 100vw, 1008px" /></figure>



<p>Nextly, let&#8217;s hit the <code>Create Bucket</code> , provide a name for it, and leave the rest as is: </p>



<figure class="wp-block-image aligncenter size-full"><img loading="lazy" decoding="async" width="831" height="692" src="http://blog.codersee.com/wp-content/uploads/2024/09/2.png" alt="" class="wp-image-12009139" srcset="https://blog.codersee.com/wp-content/uploads/2024/09/2.png 831w, https://blog.codersee.com/wp-content/uploads/2024/09/2-300x250.png 300w, https://blog.codersee.com/wp-content/uploads/2024/09/2-768x640.png 768w" sizes="auto, (max-width: 831px) 100vw, 831px" /></figure>



<p>If everything succeeded, then we should see our bucket on the list: </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="435" src="http://blog.codersee.com/wp-content/uploads/2024/09/3-1024x435.png" alt="" class="wp-image-12009140" srcset="https://blog.codersee.com/wp-content/uploads/2024/09/3-1024x435.png 1024w, https://blog.codersee.com/wp-content/uploads/2024/09/3-300x127.png 300w, https://blog.codersee.com/wp-content/uploads/2024/09/3-768x326.png 768w, https://blog.codersee.com/wp-content/uploads/2024/09/3.png 1231w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>Excellent, at this point we can get back to our Spring Boot project :). </p>



<h2 class="wp-block-heading" id="h-test-s3client-connection">Test S3Client Connection</h2>



<p>With all of that done, let&#8217;s figure out whether we can connect our local app with AWS using the <em>S3Client</em>. </p>



<p>To do so, let&#8217;s create the <code>BucketController</code> class and introduce the GET endpoint: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@RestController
@RequestMapping("/buckets")
class BucketController(
  private val s3Client: S3Client
) {

  @GetMapping
  fun listBuckets(): List&lt;String> {
    val response = s3Client.listBuckets()

    return response.buckets()
      .mapIndexed { index, bucket ->
        "Bucket #${index + 1}: ${bucket.name()}"
      }
  }
}</pre>



<p>As we can see, the above logic will be responsible for exposing the <code>GET /buckets</code> endpoint and returning a list of bucket names as &#8220;Bucket #N: some-name&#8221;. </p>



<p>Moreover, the starter we are using <strong>automatically configures and registers an S3Client bean in the Spring Boot context</strong>. So, we simply inject that without any previous configuration. </p>



<p>Anyway, less talkie-talkie, and let&#8217;s test the endpoint: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request GET 'http://localhost:8080/test' </pre>



<p>And depending on our local environment, we <strong>get the 200 OK with a bucket name: </strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">[
    "Bucket #1: your-awesome-name"
]</pre>



<p>Or the <strong>error related to <em>AwsCredentialsProviderChain</em>:</strong> </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>software.amazon.awssdk.core.exception.SdkClientException: Unable to load credentials from any of the providers in the chain AwsCredentialsProviderChain(credentialsProviders=[SystemPropertyCredentialsProvider(), EnvironmentVariableCredentialsProvider(), WebIdentityTokenCredentialsProvider(), ProfileCredentialsProvider(profileName=default, profileFile=ProfileFile(sections=[])), ContainerCredentialsProvider(), InstanceProfileCredentialsProvider()]) : [SystemPropertyCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId)., EnvironmentVariableCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId)., WebIdentityTokenCredentialsProvider(): Either the environment variable AWS_WEB_IDENTITY_TOKEN_FILE or the javaproperty aws.webIdentityTokenFile must be set., ProfileCredentialsProvider(profileName=default, profileFile=ProfileFile(sections=[])): Profile file contained no credentials for profile &#8216;default&#8217;: ProfileFile(sections=[]), ContainerCredentialsProvider(): Cannot fetch credentials from container &#8211; neither AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables are set., InstanceProfileCredentialsProvider(): Failed to load credentials from IMDS.]</p>
</blockquote>



<p>Or even: </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [software.amazon.awssdk.services.s3.S3ClientBuilder]: Factory method &#8216;s3ClientBuilder&#8217; threw exception with message: Unable to load region from any of the providers in the chain software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain@15f35bc3: [software.amazon.awssdk.regions.providers.SystemSettingsRegionProvider@2bfb583b: Unable to load region from system settings. Region must be specified either via environment variable (AWS_REGION) or system property (aws.region)., software.amazon.awssdk.regions.providers.AwsProfileRegionProvider@7301eebe: No region provided in profile: default, software.amazon.awssdk.regions.providers.InstanceProfileRegionProvider@76a805b7: Unable to contact EC2 metadata service.]</p>
</blockquote>



<p>So, let&#8217;s learn why it worked (or not🙂).</p>



<h2 class="wp-block-heading" id="h-configuring-aws-credentials">Configuring AWS Credentials </h2>



<p>Long story short, the Spring Cloud AWS starter configures the <code>DefaultCredentialsProvider</code> that looks for credentials in the following order:</p>



<ol class="wp-block-list">
<li>Java System Properties &#8211; <code>aws.accessKeyId</code> and <code>aws.secretAccessKey</code></li>



<li>Environment Variables &#8211; <code>AWS_ACCESS_KEY_ID</code> and <code>AWS_SECRET_ACCESS_KEY</code></li>



<li>Web Identity Token credentials from system properties or environment variables</li>



<li>Credential profiles file at the default location (<code>~/.aws/ credentials</code>) shared by all AWS SDKs and the AWS CLI</li>



<li>Credentials delivered through the Amazon EC2 container service if <code>AWS_CONTAINER_CREDENTIALS_RELATIVE_URI</code> environment variable is set and the security manager has permission to access the variable,</li>



<li>Instance profile credentials delivered through the Amazon EC2 metadata service</li>
</ol>



<p>And <strong>if you got 200 OK</strong>, but you don&#8217;t remember specifying anything on your local machine, then I am pretty sure that the <strong>number 4</strong>&nbsp; is the answer here 😉 </p>



<p>The <code>.aws</code> folder inside the home directory is a default location for credentials, and you could have populated that unconsciously, for example <strong>when configuring AWS CLI with <code>aws configure</code>.</strong> And the <code>DefaultCredentialsProvider</code> was smart enough to use it without your knowledge. </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>The autoconfiguration is a wonderful thing, but as we can see, it can backfire sometimes 🙂 </p>
</blockquote>



<p>On the other hand, if none of these 6 satisfies you, then <strong>Spring Cloud AWS allows us to use the access keys, too.</strong>  </p>



<p>To do so, the only thing we need to do is add the following to the <code>application.yaml</code>: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">spring:
  cloud:
    aws:
      credentials:
        access-key: your-access-key
        secret-key: your-secret-key</pre>



<h2 class="wp-block-heading" id="h-configure-region">Configure Region </h2>



<p>OK, so at this point the problem related to the credentials should be gone. However, if we got the second error related to the region, then let&#8217;s see how it works internally, too. </p>



<p>Well, when we take a look at the <code>DefaultAwsRegionProviderChain</code> docs, we will see that it looks for the region in this order:</p>



<ol class="wp-block-list">
<li>Check the <code>aws.region</code> system property for the region.</li>



<li>Check the <code>AWS_REGION</code> environment variable for the region.</li>



<li>Check the {user. home}/.aws/ credentials and {user. home}/.aws/config files for the region.</li>



<li>If running in EC2, check the EC2 metadata service for the region.</li>
</ol>



<p>So, again, if we had the <code>.aws</code> credentials set, then this value came from there😉</p>



<p>And as you might already have guessed, yes, we can update that in the properties, too:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">spring:
  cloud:
    aws:
      s3:
        region: us-east-1 #default is us-west-1</pre>



<p>And at this point, when we rerun our application, then everything should be working, as expected. </p>



<h2 class="wp-block-heading" id="h-aws-s3client-operations">AWS S3Client Operations</h2>



<p>So, with all of that done, we can finally take a look at a few AWS S3Client capabilities. Of course, we will not cover all possible scenarios, so if you have a bit more specific use case, then I recommend checking the Spring Cloud AWS / AWS SDK documentation. </p>



<h3 class="wp-block-heading" id="h-list-all-buckets">List All Buckets </h3>



<p>Firstly, let&#8217;s get back to the <code>listBuckets</code> usage:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@GetMapping
fun listBuckets(): List&lt;String> {
  val response = s3Client.listBuckets()

  return response.buckets()
    .mapIndexed { index, bucket ->
      "Bucket #${index + 1}: ${bucket.name()}"
    }
}</pre>



<p>Long story short, this function returns <code>ListBucketsResponse</code> that contains a list of all buckets owned by us. It is worth mentioning that we must have the <code>s3:ListAllMyBucket</code> permission. </p>



<p>Nevertheless, <strong>I wanted to emphasize one, important thing</strong>. </p>



<p>AWS SDK methods throw exceptions quite heavily. For example, the above may result in <code>SdkException</code>, <code>S3Exception</code>, or <code>SdkClientException</code> to be thrown. In a production-ready code, we must keep that in mind, handle them according to our needs and (if necessary) translate to appropriate HTTP status codes.</p>



<h3 class="wp-block-heading" id="h-create-new-s3-bucket">Create New S3 Bucket</h3>



<p>Following, let&#8217;s expose a <code>POST /buckets</code> endpoint that will be used to create new buckets: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@PostMapping
fun createBucket(@RequestBody request: BucketRequest) {
  val createBucketRequest = CreateBucketRequest.builder()
    .bucket(request.bucketName)
    .build()

  s3Client.createBucket(createBucketRequest)
}

data class BucketRequest(val bucketName: String)</pre>



<p>This time our function looks quite different- we must prepare the <code>CreateBucketRequest</code> that we pass to the <code>createBucket</code> function. </p>



<p>And that is quite a common thing when dealing with AWS S3Client in Spring Boot. The SDK methods expect us to provide different objects of classes extending the <code>S3Request</code>, like <code>CreateBucketRequest</code>, <code>DeleteObjectRequest</code>, etc. </p>



<p>What the <code>createBucket</code> does is pretty obvious, but we must be cautious about the bucket name, because the function may throw <code>BucketAlreadyExistsException</code>, or  <code>BucketAlreadyOwnedByYouException</code>. </p>



<p>Of course, to test that, the only thing we need to do is to hit the endpoint: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request POST 'http://localhost:8080/buckets' \
--header 'Content-Type: application/json' \
--data-raw '{
    "bucketName": "your-awesome-name"
}'</pre>



<p>And if everything is fine, a new bucket should be created. </p>



<h3 class="wp-block-heading" id="h-create-object-in-the-bucket">Create Object In The Bucket</h3>



<p>So at this point, we know how to create buckets in AWS. Nevertheless, we use them to <strong>organise uploaded files</strong>. </p>



<p>And it is the right time to learn how we can upload a file: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// we must add the typealias to avoid name clash for the @RequestBody annotation :) 
typealias PutObjectRequestBody = software.amazon.awssdk.core.sync.RequestBody

@RestController
@RequestMapping("/buckets")
class BucketController(
  private val s3Client: S3Client
) {

  @PostMapping("/{bucketName}/objects")
  fun createObject(@PathVariable bucketName: String, @RequestBody request: ObjectRequest) {
    val createObjectRequest = PutObjectRequest.builder()
      .bucket(bucketName)
      .key(request.objectName)
      .build()

    val fileContent = PutObjectRequestBody.fromString(request.content)

    s3Client.putObject(createObjectRequest, fileContent)
  }

  data class ObjectRequest(val objectName: String, val content: String)
}</pre>



<p>As we can see, this time, we make use of the <code>putObject</code> and the <code>PutObjectRequest</code> (you see the pattern now 😉 ). </p>



<p>Moreover, when preparing the request we must specify both the bucket name and our object key. </p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request POST 'http://localhost:8080/buckets/your-awesome-name/objects' \
--header 'Content-Type: application/json' \
--data-raw '{
    "objectName": "file-example.txt",
    "content": "My file content"
}'</pre>



<p>As a result, a new text file named &#8220;file-example&#8221; with &#8220;My file content&#8221; in it should be created in the &#8220;your-awesome-name&#8221; bucket. </p>



<p>Of course, this is not the only method of S3Client that allows us to upload files, and sometimes the multipart upload might be a better choice for our use case. </p>



<h3 class="wp-block-heading" id="h-list-objects-from-the-bucket">List Objects From The Bucket</h3>



<p>Nextly, let&#8217;s take a look what is the content of our bucket:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@GetMapping("/{bucketName}/objects")
fun listObjects(@PathVariable bucketName: String): List&lt;String> {
  val listObjectsRequest = ListObjectsRequest.builder()
    .bucket(bucketName)
    .build()

  val response = s3Client.listObjects(listObjectsRequest)

  return response.contents()
    .map { s3Object -> s3Object.key() }
}</pre>



<p>Similarly, we build the <code>ListObjectsRequest</code> instance, we perform the request using the <code>listObjects</code> and return an array with item names.</p>



<p>And this time, when we check with the following query:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request GET 'http://localhost:8080/buckets/your-awesome-name/objects'</pre>



<p>We should get the 200 OK with the array: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">[
    "file-example.txt"
]</pre>



<h3 class="wp-block-heading" id="h-fetch-the-object-from-s3-bucket">Fetch The Object From S3 Bucket</h3>



<p>And although listing objects might be sometimes useful, I am pretty sure you would be more often interested in getting the actual object with a key: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@GetMapping("/{bucketName}/objects/{objectName}")
fun getObject(@PathVariable bucketName: String, @PathVariable objectName: String): String {
  val getObjectRequest = GetObjectRequest.builder()
    .bucket(bucketName)
    .key(objectName)
    .build()

  val response = s3Client.getObjectAsBytes(getObjectRequest)

  return response.asString(UTF_8)
}</pre>



<p>Just like in the previous examples, we prepare the request with bucket name and object key, invoke the <code>getObjectAsBytes</code> and this time we print out the content to the output: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="generic" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">curl --location --request GET 'http://localhost:8080/buckets/your-awesome-name/objects/file-example.txt'

# Response: 
"My file content"</pre>



<p>Of course, a friendly reminder that the AWS SDK throws the exceptions, and if the file does not exist, we will get the <code>NoSuchKeyException</code>. </p>



<h3 class="wp-block-heading" id="h-delete-s3-bucket">Delete S3 Bucket</h3>



<p>As the last step, let&#8217;s take a look at the logic necessary to delete a bucket: </p>



<pre class="EnlighterJSRAW" data-enlighter-language="kotlin" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">@DeleteMapping("/{bucketName}")
fun deleteBucket(@PathVariable bucketName: String) {
  val listObjectsRequest = ListObjectsRequest.builder()
    .bucket(bucketName)
    .build()

  val listObjectsResponse = s3Client.listObjects(listObjectsRequest)

  val allObjectsIdentifiers = listObjectsResponse.contents()
    .map { s3Object ->
      ObjectIdentifier.builder()
        .key(s3Object.key())
        .build()
    }

  val del = Delete.builder()
    .objects(allObjectsIdentifiers)
    .build()

  val deleteObjectsRequest = DeleteObjectsRequest.builder()
    .bucket(bucketName)
    .delete(del)
    .build()

  s3Client.deleteObjects(deleteObjectsRequest)


  val deleteBucketRequest = DeleteBucketRequest.builder()
    .bucket(bucketName)
    .build()

  s3Client.deleteBucket(deleteBucketRequest)
}</pre>



<p>As we can see, this time, we must perform our actions in a few steps.</p>



<p>Why? </p>



<p>The reason is simple: </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: software.amazon.awssdk.services.s3.model.S3Exception: The bucket you tried to delete is not empty (Service: S3, Status Code: 409&#8230;</p>
</blockquote>



<p>As we can see, the message above is pretty descriptive. Simply said- <strong>we cannot delete a bucket that is not empty.</strong> </p>



<p>So, our function utilizes the <code>listObjects</code>, so that we can get keys to delete, the <code>deleteObjects</code> to actually delete them, and <code>deleteBucket</code> to get rid of the bucket. </p>



<h2 class="wp-block-heading" id="h-summary">Summary </h2>



<p>And that&#8217;s all for this first tutorial on how to integrate your Spring Boot application with AWS and <strong>make use of the S3Client to work with AWS S3</strong>. In the upcoming articles in the series, we will expand our knowledge to work with S3 even more efficiently. </p>



<p>Of course, I highly encourage you to take a look at the remaining articles in this series: </p>



<ul class="wp-block-list">
<li><a href="https://blog.codersee.com/test-spring-boot-aws-s3-with-localstack-and-testcontainers/">#3 Test Spring Boot AWS S3 with Localstack and Testcontainers</a></li>
</ul>



<p>For the source code, as always, please refer to <a href="https://github.com/codersee-blog/spring-boot-3-kotlin-s3Client" target="_blank" rel="noreferrer noopener">this GitHub repository</a>. </p>
<p>The post <a href="https://blog.codersee.com/spring-boot-aws-s3-s3client-kotlin/">Spring Boot with AWS S3, S3Client and Kotlin</a> appeared first on <a href="https://blog.codersee.com">Codersee blog- Kotlin on the backend</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.codersee.com/spring-boot-aws-s3-s3client-kotlin/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: blog.codersee.com @ 2026-02-25 18:37:24 by W3 Total Cache
-->