llms.txt: machine-readable documentation index. Markdown version of this page is also available.

IntelliJ IDEA 2026.3 Help

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

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

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:

    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.

    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:

    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.

  4. To configure SQL assistance, open Settings | Languages & Frameworks | SQL Dialects. Click the Add button Add, select your project directory in the file chooser, and set its SQL dialect to H2.

    SQL Dialects settings with H2 assigned to src/main/resources/schema.sql so that IntelliJ IDEA highlights errors in the schema.

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

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:

    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:

    @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.

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:

    @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:

    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.

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:

    @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:

    @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.

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.
  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.

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

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 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 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.
  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.

    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:

    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.

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; } }
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> { }
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); } }
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()); } }
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); } }
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
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 );
<!DOCTYPE html> <html lang="en"> <head> <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:

To continue working on the project, learn how to write and run tests in the test-driven development tutorial and track your changes with the Git basics tutorial.

For more information about configuring and extending Spring Boot applications, refer to the Spring Boot documentation.

15 October 2026