# Spring Boot REST API tutorial, part 4: Store data in H2 and package a JAR

> For the complete documentation index, see [llms.txt](https://www.jetbrains.com/help/idea/2026.3/llms.txt).

* [1: Create and run a Spring Boot project](spring-boot-tutorial-create-project.html)

* [2: Add REST endpoints](spring-boot-tutorial-rest-endpoints.html)

* [3: Upload and download files](spring-boot-tutorial-file-upload-download.html)

* [4: Store data in H2 and package a JAR]()

Continue from [Spring Boot REST API tutorial, part 3: Upload and download files](spring-boot-tutorial-file-upload-download.html). Replace the in-memory map with an H2 database so photos survive a restart, then package the application and run it outside IntelliJ IDEA.

Procedure: Configure a file-based H2 database

You added H2 and Spring Data JDBC as project dependencies when you created the project. H2 is a database written as a Java library, and it runs inside the application. Spring Data JDBC maps your objects to database rows and supplies the SQL for repository operations.

H2 can keep data in memory, but that would lose the photos on shutdown, as the map does now. Point it at a file instead, then define a table whose columns correspond to the photo fields.

1. In `application.properties`, add a file-based data source and enable schema initialization:

```PROPERTIES
spring.datasource.url=jdbc:h2:file:~/photos-demo-db
spring.sql.init.mode=always
```

`jdbc:h2:file:` tells H2 to use a local file, `~` stands for the home directory, so `~/photos-demo-db` is the path to the file.

`spring.sql.init.mode=always` tells Spring Boot to run `schema.sql` at startup.

2. Let's restart the application to check whether the file was created. Open your home directory in Finder or Explorer.

![Finder home directory with the photos-demo-db.mv.db file that H2 created after switching the data source URL to file-based storage.](https://resources.jetbrains.com/help/img/idea/2026.3/sb_data_source_file.png)

H2 appends `.mv.db` to the name you gave it, so the file is `photos-demo-db.mv.db`.

That single file is the whole database. You can copy it and open it on another machine with Java installed, but do not forget to stop the application first.

3. The database has no tables yet. Spring Boot can create them at startup, and there are several ways to arrange it. This tutorial uses a single SQL file that Spring runs at startup.

In `src/main/resources`, create a new file called `schema.sql`:

```SQL
create table if not exists photos (
    id bigint generated by default as identity primary key,
    file_name varchar(255),
    content_type varchar(255),
    data blob
);
```

The columns match the fields on `Photo`. The database generates the identifier, and `blob` stores the image bytes.

> **Note:**
> `if not exists` allows repeated startups, but it does not update an existing table when you edit the schema. For this tutorial, to recreate the table, stop the application and delete `photos-demo-db.mv.db` from your home directory. This removes all stored photos.

4. To configure SQL assistance, open `Settings | Languages & Frameworks | SQL Dialects`. Click ![the Add button](https://resources.jetbrains.com/help/img/idea/2026.3/schema.sql so that IntelliJ IDEA highlights errors in the schema.](https://resources.jetbrains.com/help/img/idea/2026.3/sb_sql_dialect.png)

With the dialect set, IntelliJ IDEA highlights errors in the statement as you type, including missing semicolons.

Procedure: Create a Spring Data JDBC repository

The schema defines where photos are stored. A repository gives your Java code operations for reading and writing them. Spring Data uses the model mapping to translate between a `Photo` and a row in the table.

1. Create a `repository` package next to `service` and add an interface named `PhotosRepository`:

```JAVA
public interface PhotosRepository extends CrudRepository<Photo, Integer> {
}
```

`CrudRepository` supplies methods for saving, finding, and deleting photos. Its type parameters specify the model and identifier types. You do not need to write an implementing class: Spring creates one for this interface and supplies the SQL for those operations.

2. In `Photo`, add the mapping annotations (`@Table("PHOTOS")` and `@Id`) and change the identifier (`id`) field, getter, and setter to `Integer`. Keep the no-argument constructor and other accessors unchanged:

```JAVA
@Table("PHOTOS")
public class Photo {

    @Id
    private Integer id;

    private String fileName;

    private String contentType;

    @JsonIgnore
    private byte[] data;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    // no-argument constructor and the other accessors unchanged

}
```

Use `org.springframework.data.annotation.Id` and `org.springframework.data.relational.core.mapping.Table` for the imports.

`@Table("PHOTOS")` matches H2's uppercase table name, and `@Id` identifies the primary key. Other fields map by name: `fileName` becomes `file_name`, and `contentType` becomes `content_type`.

Procedure: Replace the in-memory map with the repository

The controllers already ask the service for photos. Now change where the service gets them: each map operation becomes a repository call. When the repository saves a new photo, the database generates its identifier, replacing the UUID previously generated by the service.

1. In `PhotosService`, remove the map field and add a repository field and constructor:

```JAVA
@Service
public class PhotosService {

    private final PhotosRepository photosRepository;

    public PhotosService(PhotosRepository photosRepository) {
        this.photosRepository = photosRepository;
    }

}
```

Spring injects the repository implementation, as it injects the service into the controllers.

2. Replace the service methods with these repository calls:

```JAVA
public Iterable<Photo> get() {
    return photosRepository.findAll();
}

public Photo get(Integer id) {
    return photosRepository.findById(id).orElse(null);
}

public void remove(Integer id) {
    photosRepository.deleteById(id);
}

public Photo save(String fileName, String contentType, byte[] data) {
    Photo photo = new Photo();
    photo.setFileName(fileName);
    photo.setContentType(contentType);
    photo.setData(data);
    return photosRepository.save(photo);
}
```

The repository methods have different return types from the map operations. `findAll()` returns `Iterable`, so the service exposes that type too. `findById()` returns `Optional`, which can hold a photo or be empty. Calling `orElse(null)` keeps the existing missing-photo checks in the controllers working.

`deleteById()` returns nothing, so `remove()` can no longer return the deleted photo. The next procedure adjusts the controllers to these signature changes.

`save()` returns the saved photo with its database-generated identifier, so the service no longer assigns a UUID.

Procedure: Update the controller methods

1. In `PhotosController` and `DownloadController`, change every `@PathVariable String id` to `@PathVariable Integer id`.

2. In `PhotosController`, update the list endpoint to return `Iterable`:

```JAVA
@GetMapping("/photos")
public Iterable<Photo> get() {
    return photosService.get();
}
```

The response remains a JSON array.

3. Update the delete endpoint to call the service without checking a return value:

```JAVA
@DeleteMapping("/photos/{id}")
public void delete(@PathVariable Integer id) {
    photosService.remove(id);
}
```

Deleting a missing photo now returns 200 rather than 404, because the repository delete operation does not report whether the photo existed.

Procedure: Check that photos survive a restart

Uploading and listing a photo checks the repository calls. Restarting adds the persistence check: the application has to retrieve the photo from the database file after its in-memory state is gone.

1. Restart the application and upload an image from `http://localhost:8080/upload.html`. The response contains a numeric identifier assigned by the database.

![Browser alert after uploading ij.png with the JSON response, where the id field shows the identifier 1 assigned by the H2 database.](https://resources.jetbrains.com/help/img/idea/2026.3/sb_photo_with_identifier.png)

2. Stop the application, start it again, and open `http://localhost:8080/photos`. The photo is still listed, because it is now in a file rather than in a map that the new process created empty.

![Response from localhost:8080/photos after an application restart, still listing ij.png with id 1 because it is stored in the database file.](https://resources.jetbrains.com/help/img/idea/2026.3/sb_photo_listed.png)

Use its identifier at `http://localhost:8080/download/<id>` to download the stored image.

Procedure: Package the application as an executable JAR

So far, you have started the application from IntelliJ IDEA. Packaging it produces an executable JAR containing your code, dependencies, and the embedded web server. It runs outside the IDE with a compatible Java runtime, without installing a separate application server.

1. Stop the application in IntelliJ IDEA to release port 8080 and the database file. The Maven build runs tests that open the same database, so leaving the application running can cause the build to fail because the database file is locked.

2. Open the [Maven](maven.html) tool window, expand `photos-demo | Lifecycle`, and double-click package. Wait for the build to succeed.

IntelliJ IDEA runs the build and reports progress in the [Run](run-tool-window.html) tool window. Watch for the line confirming the build succeeded — the `package` phase compiles the code and runs the tests first, so a failing test stops the build here.

![Run tool window showing the photos-demo package Maven goal completed, with the BUILD SUCCESS line confirming the JAR was built.](https://resources.jetbrains.com/help/img/idea/2026.3/sb_maven_package.png)

3. Find `photos-demo-0.0.1-SNAPSHOT.jar` in the `target` directory.

![Project tool window showing photos-demo-0.0.1-SNAPSHOT.jar and its .jar.original file in the target directory after running Maven package.](https://resources.jetbrains.com/help/img/idea/2026.3/sb_target_jar.png)

Use this executable JAR; the `.jar.original` file is the original archive without the bundled dependencies.

4. In a terminal, change to the `target` directory and run:

```BASH
java -jar photos-demo-0.0.1-SNAPSHOT.jar
```

5. Open `http://localhost:8080/photos`. The uploaded photo is still listed because this process uses the same database file in your home directory. Copying the JAR to another machine transfers the application, but not this database.

## Check your code

If your application does not work as expected, compare your files with the code below. `PhotosDemoApplication` is not listed because it does not change after Spring Initializr generates it.

```JAVA
package com.jetbrains.photos.demo.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

@Table("PHOTOS")
public class Photo {

    @Id
    private Integer id;

    private String fileName;

    private String contentType;

    @JsonIgnore
    private byte[] data;

    public Photo() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getContentType() {
        return contentType;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }

}
```

```JAVA
package com.jetbrains.photos.demo.repository;

import com.jetbrains.photos.demo.model.Photo;
import org.springframework.data.repository.CrudRepository;

public interface PhotosRepository extends CrudRepository<Photo, Integer> {
}
```

```JAVA
package com.jetbrains.photos.demo.service;

import com.jetbrains.photos.demo.model.Photo;
import com.jetbrains.photos.demo.repository.PhotosRepository;
import org.springframework.stereotype.Service;

@Service
public class PhotosService {

    private final PhotosRepository photosRepository;

    public PhotosService(PhotosRepository photosRepository) {
        this.photosRepository = photosRepository;
    }

    public Iterable<Photo> get() {
        return photosRepository.findAll();
    }

    public Photo get(Integer id) {
        return photosRepository.findById(id).orElse(null);
    }

    public void remove(Integer id) {
        photosRepository.deleteById(id);
    }

    public Photo save(String fileName, String contentType, byte[] data) {
        Photo photo = new Photo();
        photo.setFileName(fileName);
        photo.setContentType(contentType);
        photo.setData(data);
        return photosRepository.save(photo);
    }

}
```

```JAVA
package com.jetbrains.photos.demo.web;

import com.jetbrains.photos.demo.model.Photo;
import com.jetbrains.photos.demo.service.PhotosService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;

@RestController
public class PhotosController {

    private final PhotosService photosService;

    public PhotosController(PhotosService photosService) {
        this.photosService = photosService;
    }

    @GetMapping("/")
    public String hello() {
        return "Hello World";
    }

    @GetMapping("/photos")
    public Iterable<Photo> get() {
        return photosService.get();
    }

    @GetMapping("/photos/{id}")
    public Photo get(@PathVariable Integer id) {
        Photo photo = photosService.get(id);
        if (photo == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }
        return photo;
    }

    @DeleteMapping("/photos/{id}")
    public void delete(@PathVariable Integer id) {
        photosService.remove(id);
    }

    @PostMapping("/photos")
    public Photo create(@RequestPart("data") MultipartFile file) throws IOException {
        return photosService.save(file.getOriginalFilename(), file.getContentType(), file.getBytes());
    }

}
```

```JAVA
package com.jetbrains.photos.demo.web;

import com.jetbrains.photos.demo.model.Photo;
import com.jetbrains.photos.demo.service.PhotosService;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

@RestController
public class DownloadController {

    private final PhotosService photosService;

    public DownloadController(PhotosService photosService) {
        this.photosService = photosService;
    }

    @GetMapping("/download/{id}")
    public ResponseEntity<byte[]> download(@PathVariable Integer id) {
        Photo photo = photosService.get(id);
        if (photo == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.valueOf(photo.getContentType()));
        headers.setContentDisposition(
                ContentDisposition.builder("attachment")
                        .filename(photo.getFileName())
                        .build());

        return new ResponseEntity<>(photo.getData(), headers, HttpStatus.OK);
    }

}
```

```PROPERTIES
spring.application.name=photos-demo
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
spring.datasource.url=jdbc:h2:file:~/photos-demo-db
spring.sql.init.mode=always
```

```SQL
create table if not exists photos (
    id bigint generated by default as identity primary key,
    file_name varchar(255),
    content_type varchar(255),
    data blob
);
```

```HTML
<!DOCTYPE html>
<html lang="en">
<head><meta name="robots" content="noindex"/>
    <meta charset="UTF-8">
    <title>Photo Upload</title>
</head>
<body>

<h2>Photo Upload</h2>

<input id="fileupload" type="file" name="fileupload"/>
<button id="upload-button" onclick="uploadFile()">Upload</button>

<script>
    async function uploadFile() {
        let formData = new FormData();
        formData.append("data", fileupload.files[0]);
        await fetch("http://localhost:8080/photos", {
            method: "POST",
            body: formData
        }).then(result => result.text())
            .then(text => alert(text));
    }
</script>
</body>
</html>
```

## Summary and next steps

You have built a photo-sharing REST API with Spring Boot. Across the four parts of this tutorial, you:

* [Created](spring-boot-tutorial-create-project.html#create-project) and [ran](spring-boot-tutorial-create-project.html#first-run) a Spring Boot project in IntelliJ IDEA.

* Added REST endpoints to [list](spring-boot-tutorial-rest-endpoints.html#return-all-photos), [retrieve](spring-boot-tutorial-rest-endpoints.html#return-one-photo), and [delete](spring-boot-tutorial-rest-endpoints.html#delete-photo) photos.

* Separated photo storage from request handling with a [shared service and constructor injection](spring-boot-tutorial-rest-endpoints.html#create-service).

* Added [file uploads](spring-boot-tutorial-file-upload-download.html#receive-file) and a [download endpoint](spring-boot-tutorial-file-upload-download.html#serve-image) that returns images with their original file names.

* [Stored photos](#rewrite-service) in H2 with Spring Data JDBC and [checked](#check-round-trip) that they survive an application restart.

* [Built](#build-jar) an executable JAR with Maven and ran the application outside the IDE.

To continue working on the project, learn how to write and run tests in the [test-driven development tutorial](tdd-with-intellij-idea.html) and track your changes with the [Git basics tutorial](working-with-git-tutorial.html).

For more information about configuring and extending Spring Boot applications, refer to the [Spring Boot documentation](https://docs.spring.io/spring-boot/index.html).

