Get an Array of Data from a Java Servlet in JavaScript

Sending an array of data from a Java servlet to JavaScript running in a browser comes down to three steps: render the array as JSON on the server, set the correct response headers, and parse it on the client. The modern tooling makes each step straightforward.

Server side — the servlet

Use a real JSON library (Jackson or Gson) rather than hand-written string concatenation. Escaping, Unicode and edge cases are easy to get wrong manually.

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.18.2</version>
</dependency>
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.*;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.util.List;
import java.util.Map;

@WebServlet("/api/users")
public class UserServlet extends HttpServlet {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        List<Map<String, Object>> users = List.of(
            Map.of("id", 1, "name", "Alice", "admin", true),
            Map.of("id", 2, "name", "Bob",   "admin", false),
            Map.of("id", 3, "name", "Carol", "admin", false)
        );

        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");
        MAPPER.writeValue(resp.getWriter(), users);
    }
}

Client side — fetch API

async function loadUsers() {
    try {
        const response = await fetch('/api/users');
        if (!response.ok) throw new Error('HTTP ' + response.status);
        const users = await response.json();

        const ul = document.getElementById('userList');
        ul.innerHTML = '';
        for (const u of users) {
            const li = document.createElement('li');
            li.textContent = u.name + (u.admin ? ' (admin)' : '');
            ul.appendChild(li);
        }
    } catch (err) {
        console.error('Failed to load users:', err);
    }
}

document.addEventListener('DOMContentLoaded', loadUsers);

Render into a table

const users = await (await fetch('/api/users')).json();
const table = document.getElementById('users');
table.innerHTML = '<tr><th>ID</th><th>Name</th><th>Admin</th></tr>';
users.forEach(u => {
    const row = table.insertRow();
    row.insertCell().textContent = u.id;
    row.insertCell().textContent = u.name;
    row.insertCell().textContent = u.admin ? 'Yes' : 'No';
});

Send parameters: JSON POST

For create/update operations, post JSON from the browser and parse it on the servlet:

async function createUser(name) {
    const res = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name })
    });
    if (!res.ok) throw new Error('Create failed');
    return res.json();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    Map<String, Object> body = MAPPER.readValue(req.getReader(), Map.class);
    String name = (String) body.get("name");
    // Save...
    resp.setContentType("application/json");
    resp.setStatus(201);
    MAPPER.writeValue(resp.getWriter(), Map.of("id", 42, "name", name));
}

Typed DTOs

Instead of Map, declare a typed class and let Jackson bind automatically:

public record User(int id, String name, boolean admin) { }

List<User> users = List.of(
    new User(1, "Alice", true),
    new User(2, "Bob",   false)
);
MAPPER.writeValue(resp.getWriter(), users);

Handling encoding correctly

Always set Content-Type: application/json; charset=UTF-8. Without it, accented characters can display as café instead of café. Jackson writes UTF-8 by default when you give it a Writer.

CORS — if the browser and the servlet are on different origins

Your Java app might run on api.myapp.com while the front-end comes from myapp.com. Enable CORS via a simple filter:

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter("/api/*")
public class CorsFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
        HttpServletResponse r = (HttpServletResponse) res;
        r.setHeader("Access-Control-Allow-Origin", "https://myapp.com");
        r.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
        r.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
        chain.doFilter(req, res);
    }
}

Do not use * in production when you pass credentials — restrict to known origins.

Handling errors cleanly

Return a JSON error body plus a proper status code:

try {
    // ...
} catch (SQLException e) {
    resp.setStatus(500);
    resp.setContentType("application/json");
    MAPPER.writeValue(resp.getWriter(), Map.of(
        "error", "database_error",
        "message", e.getMessage()
    ));
}

On the client:

const res = await fetch('/api/users');
if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err.message || `HTTP ${res.status}`);
}

Security reminders

  • Escape user-provided values before injecting them into the DOM — textContent is safe; innerHTML requires sanitising.
  • Validate and authorise on the server — the client is never trusted.
  • Use HTTPS in production; session cookies must be HttpOnly and Secure.

Once the JSON handshake works, you have a clean foundation for any Java back-end + JavaScript front-end combination, whether you stick with vanilla servlets or move on to Spring Boot, Micronaut or Quarkus.