Jersey REST API – Complete File Upload and Download Example

File upload is one of those features that sounds simple until you actually implement it — multipart boundaries, streaming vs buffering, size limits, and MIME-type validation all add up quickly. In this tutorial you will build a complete file-upload REST endpoint using Jersey 3.x (JAX-RS 3.1), test it with an HTML form and with curl, enforce a file-size limit, and save uploads to a configurable directory on disk.

Maven Dependencies

<!-- Jersey core -->
<dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-servlet</artifactId>
    <version>3.1.5</version>
</dependency>

<!-- Multipart support -->
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-multipart</artifactId>
    <version>3.1.5</version>
</dependency>

<!-- Jackson JSON -->
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>3.1.5</version>
</dependency>

Register the Multipart Feature

import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import jakarta.ws.rs.ApplicationPath;

@ApplicationPath("/api")
public class AppConfig extends ResourceConfig {
    public AppConfig() {
        packages("com.ankurm");
        register(MultiPartFeature.class);   // required for multipart/form-data
    }
}

Single-File Upload Endpoint

package com.ankurm.upload;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import org.glassfish.jersey.media.multipart.*;

import java.io.*;
import java.nio.file.*;
import java.util.UUID;

@Path("/files")
public class FileUploadResource {

    /** Directory where uploaded files are saved */
    private static final Path UPLOAD_DIR = Paths.get(System.getProperty("java.io.tmpdir"), "uploads");

    /** Maximum allowed file size: 10 MB */
    private static final long MAX_FILE_SIZE = 10 * 1024 * 1024L;

    // Ensure upload directory exists at startup
    static {
        try { Files.createDirectories(UPLOAD_DIR); }
        catch (IOException e) { throw new ExceptionInInitializerError(e); }
    }

    /**
     * POST /api/files/upload
     * Accepts: multipart/form-data with a "file" part
     */
    @POST
    @Path("/upload")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces(MediaType.APPLICATION_JSON)
    public Response upload(
        @FormDataParam("file") InputStream fileStream,
        @FormDataParam("file") FormDataContentDisposition fileInfo,
        @FormDataParam("description") String description) throws IOException {

        if (fileStream == null || fileInfo == null) {
            return Response.status(400)
                .entity("{ "error": "No file part found in request" }")
                .build();
        }

        // Read entire input into a byte array so we can check size
        byte[] bytes = fileStream.readAllBytes();

        if (bytes.length > MAX_FILE_SIZE) {
            return Response.status(413)
                .entity("{ "error": "File exceeds 10 MB limit" }")
                .build();
        }

        // Generate a unique stored filename to avoid collisions
        String originalName  = fileInfo.getFileName();
        String extension     = originalName.contains(".") ?
            originalName.substring(originalName.lastIndexOf('.')) : "";
        String storedName    = UUID.randomUUID() + extension;
        Path   destination   = UPLOAD_DIR.resolve(storedName);

        Files.write(destination, bytes);

        String json = String.format(
            "{ "originalName": "%s", "storedAs": "%s"," +
            " "sizeBytes": %d, "description": "%s" }",
            originalName, storedName, bytes.length,
            description != null ? description : ""
        );
        return Response.status(201).entity(json).build();
    }
}

Multiple Files Upload Endpoint

@POST
@Path("/upload-multiple")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response uploadMultiple(FormDataMultiPart multiPart) throws IOException {

    var parts  = multiPart.getFields("files");
    if (parts == null || parts.isEmpty()) {
        return Response.status(400)
            .entity("{ "error": "No 'files' parts found" }")
            .build();
    }

    StringBuilder results = new StringBuilder("[ ");

    for (FormDataBodyPart part : parts) {
        FormDataContentDisposition info  = part.getFormDataContentDisposition();
        byte[]                     bytes = part.getEntityAs(byte[].class);
        String original = info.getFileName();
        String ext      = original.contains(".") ? original.substring(original.lastIndexOf('.')) : "";
        String stored   = UUID.randomUUID() + ext;
        Files.write(UPLOAD_DIR.resolve(stored), bytes);
        results.append(String.format(
            "{ "original": "%s", "stored": "%s", "bytes": %d }, ",
            original, stored, bytes.length));
    }

    // Remove trailing comma
    if (results.length() > 2) results.setLength(results.length() - 2);
    results.append(" ]");

    return Response.status(201).entity(results.toString()).build();
}

File Download Endpoint

@GET
@Path("/download/{filename}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response download(@PathParam("filename") String filename) throws IOException {

    // Sanitise: prevent path traversal
    if (filename.contains("..") || filename.contains("/")) {
        return Response.status(400).entity("Invalid filename").build();
    }

    Path file = UPLOAD_DIR.resolve(filename);
    if (!Files.exists(file)) {
        return Response.status(404).entity("File not found").build();
    }

    return Response.ok(Files.newInputStream(file))
        .header("Content-Disposition", "attachment; filename="" + filename + """)
        .build();
}

HTML Form for Testing

<!DOCTYPE html>
<html>
<body>
  <h2>Upload a File</h2>
  <form action="http://localhost:8080/api/files/upload"
        method="post" enctype="multipart/form-data">
    <label>File: <input type="file" name="file" required></label><br><br>
    <label>Description: <input type="text" name="description"></label><br><br>
    <button type="submit">Upload</button>
  </form>
</body>
</html>

Testing with curl

# Single file upload
curl -X POST http://localhost:8080/api/files/upload 
     -F "file=@/path/to/report.pdf" 
     -F "description=Monthly report"

# Multiple files
curl -X POST http://localhost:8080/api/files/upload-multiple 
     -F "[email protected]" 
     -F "[email protected]"

# Download
curl -O http://localhost:8080/api/files/download/<stored-filename>

Sample Response

HTTP/1.1 201 Created
Content-Type: application/json

{
  "originalName": "report.pdf",
  "storedAs": "3f7a1b2c-...uuid...-a9.pdf",
  "sizeBytes": 204800,
  "description": "Monthly report"
}

Production Considerations

  • Stream large filesreadAllBytes() loads the entire file into heap. For files > 50 MB, stream directly to disk using Files.copy(inputStream, destination).
  • Validate MIME type — check part.getMediaType() or use Apache Tika to sniff the actual content type; do not trust the client-supplied Content-Type.
  • Use cloud storage for scalability — in a multi-instance environment save files to AWS S3, Google Cloud Storage, or Azure Blob instead of the local filesystem.
  • Scan for malware — pass uploaded files through a virus scanner (e.g. ClamAV) before serving them to other users.
  • Enforce authentication — combine with the JWT filter from the Jersey Security Guide to protect the upload endpoint.

See Also

Conclusion

Building a file-upload endpoint in Jersey requires registering MultiPartFeature, using @FormDataParam to bind the stream and content-disposition, and generating a UUID-based stored filename to avoid collisions. The same pattern scales from single to multiple files with minimal code changes. For production, swap local disk storage for a cloud object store, enforce strict MIME-type validation, and always authenticate upload requests.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.