JSP, JSTL & the View Layer

JSTL Core Tags

18 min Lesson 5 of 13

JSTL Core Tags

One of the cardinal sins of early JSP development was embedding Java code directly inside view files using scriptlets. The result was templates that were impossible to hand to a designer, painful to test, and fragile to maintain. The JSP Standard Tag Library (JSTL) was introduced specifically to solve this problem by replacing scriptlets with a vocabulary of XML-like tags. The Core tag library — prefix c: — covers the everyday need: conditionals, iteration, and scoped variables. Master these four tags and your JSPs become readable, testable, and design-friendly.

Adding JSTL to Your Project

JSTL is not part of the Jakarta EE servlet spec itself; you need to add the implementation JAR. With Maven:

<!-- Jakarta EE 10 / Tomcat 10+ --> <dependency> <groupId>org.glassfish.web</groupId> <artifactId>jakarta.servlet.jsp.jstl</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>jakarta.servlet.jsp.jstl</groupId> <artifactId>jakarta.servlet.jsp.jstl-api</artifactId> <version>3.0.0</version> </dependency>

Then declare the Core taglib at the top of every JSP that uses it:

<%@ taglib uri="jakarta.tags.core" prefix="c" %>
Jakarta vs javax namespace: If you are on Tomcat 10+ / Jakarta EE 10+, use the jakarta.tags.core URI. On older Tomcat 9 / Java EE projects, the URI is http://java.sun.com/jsp/jstl/core. Always match the JSTL version to your servlet container version.

c:set — Storing Values in Scope

<c:set> is the JSTL replacement for a scriptlet variable declaration. It stores a value in one of the four JSP scopes: page, request, session, or application. The default scope is page.

<%-- Create a page-scoped variable --%> <c:set var="greeting" value="Hello, World!" /> <%-- Store a computed EL expression --%> <c:set var="itemCount" value="${cart.items.size()}" scope="request" /> <%-- Assign body content as the value --%> <c:set var="welcomeMsg"> Welcome back, ${user.firstName}! </c:set> <p>${greeting}</p> <p>You have ${itemCount} item(s) in your cart.</p>

A particularly useful pattern is using c:set to promote a request attribute into session scope after an action, or to create a local alias for a deeply nested property so you do not repeat the full EL path throughout the page.

Use c:set as an alias: If you reference ${order.customer.billingAddress.city} five times on a page, set it once — <c:set var="city" value="${order.customer.billingAddress.city}"/> — then use ${city}. Shorter, faster (one EL evaluation), and easier to rename.

c:if — Simple Conditionals

<c:if> renders its body only when the test attribute evaluates to true. It is the straightforward one-branch conditional.

<c:if test="${not empty user}"> <p>Welcome, <strong>${user.displayName}</strong>!</p> <a href="/logout">Log out</a> </c:if> <c:if test="${empty user}"> <a href="/login">Log in</a> </c:if> <%-- Numeric comparison --%> <c:if test="${product.stock gt 0}"> <button class="btn-add-to-cart">Add to Cart</button> </c:if> <c:if test="${product.stock eq 0}"> <span class="out-of-stock">Out of Stock</span> </c:if>

Notice the EL operators: empty checks for null or an empty collection/string, not empty is its inverse, gt means greater-than, and eq means equals. Using these textual operators avoids the need to escape < and > inside XML/HTML.

c:if has no else. If you need an if/else construct, you need c:choose. Using two opposing c:if tests is fragile — if the condition changes, you must update both tags and risk introducing a gap or an overlap.

c:choose — Multi-Branch Conditionals

<c:choose> with nested <c:when> and <c:otherwise> is JSTL's equivalent of a switch/if-else chain. Only the first matching when block is rendered; otherwise is the fallback if nothing matches.

<c:choose> <c:when test="${order.status eq 'PENDING'}"> <span class="badge badge-warning">Pending Review</span> </c:when> <c:when test="${order.status eq 'CONFIRMED'}"> <span class="badge badge-info">Confirmed</span> </c:when> <c:when test="${order.status eq 'SHIPPED'}"> <span class="badge badge-primary">Shipped</span> </c:when> <c:when test="${order.status eq 'DELIVERED'}"> <span class="badge badge-success">Delivered</span> </c:when> <c:otherwise> <span class="badge badge-secondary">${order.status}</span> </c:otherwise> </c:choose>

The c:otherwise block serves a double purpose: it renders a sensible default AND it acts as a safety net when new enum values are added to the backend without a matching c:when. Always include it.

c:forEach — Iteration

<c:forEach> is JSTL's iterator. It works over any java.lang.Iterable, arrays, Map entries, and comma-separated strings. It is the single most frequently used JSTL tag in a typical web application.

Basic list rendering:

<table class="product-table"> <thead> <tr><th>Name</th><th>Price</th><th>Stock</th></tr> </thead> <tbody> <c:forEach var="product" items="${products}"> <tr> <td>${product.name}</td> <td>$${product.price}</td> <td>${product.stock}</td> </tr> </c:forEach> </tbody> </table>

The varStatus attribute exposes a loop-status object with useful properties:

<c:forEach var="item" items="${cartItems}" varStatus="status"> <div class="cart-row ${status.last ? 'last-row' : ''}"> <span class="row-num">${status.count}</span> <%-- 1-based counter --%> <span>${item.product.name}</span> <span>${item.quantity} x $${item.product.price}</span> <c:if test="${not status.last}"> <hr class="divider"/> </c:if> </div> </c:forEach>

varStatus properties at a glance:

  • index — zero-based index of the current iteration
  • count — one-based count (index + 1), great for row numbers
  • first — boolean, true on the first iteration
  • last — boolean, true on the final iteration

Ranging over integers (no collection needed):

<%-- Print pagination links 1 through totalPages --%> <c:forEach var="page" begin="1" end="${totalPages}"> <a href="?page=${page}" class="${page eq currentPage ? 'active' : ''}">${page}</a> </c:forEach>

Iterating a Map:

<%-- request attribute: Map<String, Integer> categoryCounts --%> <ul> <c:forEach var="entry" items="${categoryCounts}"> <li>${entry.key}: ${entry.value} product(s)</li> </c:forEach> </ul>

Combining Tags: A Complete Example

Real JSPs combine all four tags. Here is a realistic product listing page fragment showing a Servlet placing attributes into the request, and the JSP rendering them without a single scriptlet:

<%-- Servlet sets: List<Product> products, String category, int totalCount --%> <c:set var="hasProducts" value="${not empty products}" /> <h1> <c:choose> <c:when test="${not empty category}"> ${category} Products </c:when> <c:otherwise>All Products</c:otherwise> </c:choose> </h1> <c:if test="${hasProducts}"> <p class="result-count">Showing ${products.size()} of ${totalCount}</p> <div class="product-grid"> <c:forEach var="p" items="${products}" varStatus="s"> <div class="product-card ${s.first ? 'featured' : ''}"> <img src="${p.imageUrl}" alt="${p.name}" /> <h3>${p.name}</h3> <p class="price">$${p.price}</p> <c:choose> <c:when test="${p.stock gt 10}"> <span class="in-stock">In Stock</span> </c:when> <c:when test="${p.stock gt 0}"> <span class="low-stock">Only ${p.stock} left!</span> </c:when> <c:otherwise> <span class="out-of-stock">Out of Stock</span> </c:otherwise> </c:choose> </div> </c:forEach> </div> </c:if> <c:if test="${not hasProducts}"> <p class="empty-state">No products found. <a href="/products">Browse all</a></p> </c:if>
Prefer EL operators over Java method calls in tests. Writing ${products.size() gt 0} works, but ${not empty products} is idiomatic JSTL and handles null gracefully — size() on a null reference throws a NullPointerException.

Why These Tags Belong in Every JSP

These four Core tags implement the logic-free view principle: business decisions (what data to show, how to compute a total) belong in the Servlet or service layer; the JSP's only job is to render a given state. c:if/c:choose express display decisions such as "show an empty-state message" or "add a CSS class when this is the first item". c:forEach renders collections prepared by the controller. c:set creates local aliases to keep the template readable. Together they let a designer edit the HTML without touching any Java, and let a developer unit-test all logic before the view is even written.

Summary

  • c:set — declare a scoped variable or alias; eliminates scriptlet <% %> declarations.
  • c:if — single-branch conditional; use when you only need a positive guard.
  • c:choose / c:when / c:otherwise — multi-branch conditional; the clean alternative to cascading c:if pairs.
  • c:forEach — iterate any collection, array, map, or integer range; use varStatus for index, count, first/last flags.

In the next lesson you will extend JSTL with the Formatting and Functions tag libraries to handle dates, numbers, and string manipulation — keeping formatting logic out of your Java code as well.