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 —
textContentis safe;innerHTMLrequires sanitising. - Validate and authorise on the server — the client is never trusted.
- Use HTTPS in production; session cookies must be
HttpOnlyandSecure.
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.