A RESTful API built with JAX-RS (Jersey) for managing campus rooms, IoT sensors, and sensor readings as part of the university's SmartCampus initiative.
- API Design Overview
- Build & Run Instructions
- Sample curl Commands
- Report: Conceptual Questions & Answers
The SmartCampus API follows a resource-oriented architecture aligned with RESTful principles. It manages three core entities:
| Resource | Base Path | Description |
|---|---|---|
| Room | /api/v1/rooms |
Physical rooms on campus |
| Sensor | /api/v1/sensors |
IoT devices deployed in rooms |
| SensorReading | /api/v1/sensors/{id}/readings |
Historical measurement data |
- JAX-RS (Jersey) with Grizzly embedded HTTP server
- In-memory data storage using
ConcurrentHashMapandArrayList - Sub-Resource Locator Pattern for sensor readings (clean delegation)
- Custom Exception Mappers for HTTP 409, 422, 403, and 500
- Request/Response Logging Filter for API observability
- HATEOAS-style Discovery Endpoint at
GET /api/v1
/api/v1 → API Discovery & Metadata
/api/v1/rooms → Room Collection (GET, POST)
/api/v1/rooms/{roomId} → Individual Room (GET, DELETE)
/api/v1/sensors → Sensor Collection (GET, POST)
/api/v1/sensors?type=CO2 → Filtered Sensor Collection
/api/v1/sensors/{sensorId} → Individual Sensor (GET)
/api/v1/sensors/{sensorId}/readings → Sensor Reading History (GET, POST)
- Java 11 or later (JDK)
- Apache Maven 3.6+
git clone https://github.com/<your-username>/smartcampus-api.git
cd smartcampus-apimvn clean compilemvn exec:javaThe server will start on http://localhost:8080. You should see:
SmartCampus API is RUNNING
Base URI : http://localhost:8080/
API Root : http://localhost:8080/api/v1
mvn clean package
java -jar target/smartcampus-api-1.0-SNAPSHOT.jarOpen a new terminal and use the curl commands below to interact with the API.
curl -s http://localhost:8080/api/v1 | python3 -m json.toolcurl -s -X POST http://localhost:8080/api/v1/rooms \
-H "Content-Type: application/json" \
-d '{"id": "CS-401", "name": "Computer Science Lab", "capacity": 40}' \
| python3 -m json.toolcurl -s http://localhost:8080/api/v1/rooms | python3 -m json.toolcurl -s -X POST http://localhost:8080/api/v1/sensors \
-H "Content-Type: application/json" \
-d '{"id": "LIGHT-001", "type": "Lighting", "status": "ACTIVE", "currentValue": 750.0, "roomId": "CS-401"}' \
| python3 -m json.toolcurl -s -X POST http://localhost:8080/api/v1/sensors/TEMP-001/readings \
-H "Content-Type: application/json" \
-d '{"value": 23.7}' \
| python3 -m json.toolcurl -s "http://localhost:8080/api/v1/sensors?type=Temperature" | python3 -m json.toolcurl -s -X DELETE http://localhost:8080/api/v1/rooms/LIB-301 | python3 -m json.toolcurl -s -X POST http://localhost:8080/api/v1/sensors \
-H "Content-Type: application/json" \
-d '{"id": "ERR-001", "type": "Temperature", "status": "ACTIVE", "currentValue": 0, "roomId": "NONEXISTENT"}' \
| python3 -m json.toolcurl -s -X POST http://localhost:8080/api/v1/sensors/TEMP-002/readings \
-H "Content-Type: application/json" \
-d '{"value": 19.5}' \
| python3 -m json.toolThe default lifecycle of a JAX-RS resource class is request-scoped (also known as per-request). This means the JAX-RS runtime (e.g., Jersey) creates a new instance of each resource class for every incoming HTTP request. Once the request is processed and the response is sent, the instance is eligible for garbage collection.
Impact on In-Memory Data Structures:
Because each request gets its own resource instance, any data stored as instance variables within a resource class would be lost after each request. This is why we must use a shared, external data store — in our implementation, a thread-safe singleton DataStore class that uses ConcurrentHashMap.
To prevent data loss and race conditions:
- We use
ConcurrentHashMapinstead ofHashMapbecause multiple request threads may read/write concurrently - The
DataStoreis a singleton accessed via a staticgetInstance()method, ensuring all resource instances share the same data ConcurrentHashMapprovides atomic operations likeputIfAbsent()andcomputeIfAbsent()to prevent race conditions without explicit synchronisation
If we had used @Singleton on the resource class instead, a single instance would serve all requests. While this reduces object creation overhead, it introduces concurrency risks because multiple threads could simultaneously modify instance fields, leading to data corruption if not properly synchronised.
HATEOAS (Hypermedia as the Engine of Application State) is considered a hallmark of advanced RESTful design because it makes APIs self-documenting and navigable. Instead of requiring clients to hardcode every endpoint URL from static documentation, the API itself provides links to related resources and available actions within each response.
Benefits over static documentation:
-
Discoverability: Clients can navigate the entire API by following links from the root endpoint, similar to how a user navigates a website by clicking links. Our discovery endpoint at
GET /api/v1provides a map of all primary resource collections. -
Decoupling: Client implementations become loosely coupled to specific URL structures. If the server changes a URL path (e.g., from
/api/v1/roomsto/api/v2/rooms), HATEOAS-driven clients adapt automatically by following updated links rather than breaking. -
Reduced Errors: Static documentation can become outdated. Hypermedia links are always in sync with the server because they are generated at runtime.
-
Guided Workflows: The server can conditionally include or exclude links based on the current resource state, effectively guiding the client through valid state transitions (e.g., only showing a "delete" link when deletion is permitted).
-
Self-Contained Responses: Each response contains everything the client needs to take the next step, eliminating the need to cross-reference external documentation.
When returning a list of rooms, there are significant trade-offs between returning only resource IDs versus returning the full room objects:
Returning Full Objects (our implementation):
- Pros: Reduces the number of HTTP round-trips. The client receives all data in a single request, which is more efficient for client-side rendering and processing. This is especially beneficial for mobile clients where network latency is high.
- Cons: Larger response payloads consume more bandwidth. If the client only needs a subset of the data (e.g., just room names), the extra fields represent wasted bandwidth.
Returning IDs Only:
- Pros: Minimal payload size. The client can selectively fetch only the rooms it needs, which is efficient when dealing with very large collections where the client typically only needs a few items.
- Cons: Leads to the N+1 problem — the client must make one request for the list, then N additional requests to fetch each room's details. This dramatically increases network overhead and latency, especially over slow connections.
For the SmartCampus API, returning full objects is the better choice because campus management applications typically need to display complete room information in dashboards, and the total number of rooms is manageable enough that payload size is not a concern.
Yes, the DELETE operation is idempotent in our implementation.
Idempotency means that making the same request multiple times produces the same server-side effect as making it once. Here is what happens when a client sends the same DELETE request multiple times:
-
First call: The room exists and has no sensors. The server removes the room from the data store and returns HTTP 204 No Content. Server state change: room is deleted.
-
Second call (and beyond): The room no longer exists. The server looks up the room, finds nothing, and returns HTTP 404 Not Found. Server state: unchanged (room is still absent).
The key insight is that while the HTTP status codes differ (204 vs 404), the server-side state after any number of identical calls is identical: the room does not exist. The purpose of idempotency is to guarantee that retrying a request (e.g., due to network timeout) does not cause unintended side effects like deleting a different room or corrupting data. Our implementation satisfies this guarantee.
This is consistent with the HTTP specification (RFC 7231), which states that the server state after N>0 identical requests should be the same as after a single request.
When we annotate a POST method with @Consumes(MediaType.APPLICATION_JSON), we explicitly declare that this endpoint only accepts application/json request bodies.
Technical consequences of sending the wrong Content-Type:
If a client sends data with a Content-Type of text/plain or application/xml, the JAX-RS runtime will reject the request before it even reaches our resource method. The runtime performs content negotiation by comparing the request's Content-Type header against the @Consumes annotation:
-
Mismatch detected: JAX-RS finds no matching
MessageBodyReadercapable of deserialising the incoming media type into the expected Java object. -
HTTP 415 Unsupported Media Type: The runtime automatically returns this status code to the client, indicating that the server refuses to accept the request because the payload format is not supported by the target resource for the requested method.
-
No custom error handling needed: This validation happens at the framework level, before any business logic executes. The resource method is never invoked.
This is a powerful feature of JAX-RS because it provides automatic input validation at the protocol level, ensuring that our business logic only receives properly formatted JSON data. It also follows the principle of "fail fast" — the client is informed immediately about the format mismatch rather than receiving a confusing deserialization error.
Our implementation uses @QueryParam("type") for filtering sensors (e.g., GET /sensors?type=CO2). An alternative design would embed the type in the URL path (e.g., GET /sensors/type/CO2).
Why query parameters are superior for filtering and searching collections:
-
Optionality: Query parameters are inherently optional.
GET /sensorsreturns all sensors, whileGET /sensors?type=CO2returns only CO2 sensors. With path-based design, you would need two separate endpoints (/sensorsand/sensors/type/{type}), increasing code duplication and routing complexity. -
Composability: Query parameters can be combined freely. For example,
GET /sensors?type=CO2&status=ACTIVEallows multi-criteria filtering without complex path structures. Path-based designs would require increasingly awkward nested paths like/sensors/type/CO2/status/ACTIVE. -
Resource Identity: In RESTful design, the URL path identifies a resource.
/sensorsidentifies the sensor collection — it's the same collection whether filtered or not. Filtering criteria should not change the resource identity; they are modifiers on how the collection is presented. Query parameters correctly express this semantic distinction. -
Cacheability: Proxies and CDNs can cache responses based on the full URL including query strings. Different filter combinations produce different cache entries naturally.
-
Convention: The HTTP specification and industry standards (e.g., Google, GitHub, Twitter APIs) universally use query parameters for filtering, sorting, and pagination. Using path parameters for these purposes would violate developer expectations and make the API harder to use.
The Sub-Resource Locator pattern is a powerful architectural tool in JAX-RS that allows a parent resource class to delegate handling of nested paths to a dedicated child resource class.
In our implementation, SensorResource delegates {sensorId}/readings to SensorReadingResource:
@Path("/{sensorId}/readings")
public SensorReadingResource getReadingsResource(@PathParam("sensorId") String sensorId) {
return new SensorReadingResource(sensorId);
}Architectural benefits:
-
Separation of Concerns: Each resource class is responsible for exactly one logical entity.
SensorResourcehandles sensor CRUD, whileSensorReadingResourcehandles reading history. Neither class needs to know the internal implementation details of the other. -
Reduced Complexity: Without sub-resource locators, a single
SensorResourceclass would need to contain methods forGET /sensors,POST /sensors,GET /sensors/{id},GET /sensors/{id}/readings,POST /sensors/{id}/readings, and potentiallyGET /sensors/{id}/readings/{readingId}. This creates a monolithic "god class" that is difficult to read, test, and maintain. -
Improved Testability: Smaller, focused classes can be unit-tested independently.
SensorReadingResourcecan be tested in isolation by simply constructing it with a sensor ID, without setting up the full sensor routing infrastructure. -
Team Scalability: In larger projects, different team members can work on different sub-resource classes simultaneously without merge conflicts or stepping on each other's code.
-
Reusability: Sub-resource classes can potentially be reused. For example, if readings needed to be accessible from another path, the same
SensorReadingResourceclass could be instantiated from a different parent locator. -
Encapsulation of Context: The locator method captures the parent context (the
sensorId) and passes it to the sub-resource, establishing a clear parent-child relationship. This ensures the sub-resource always operates in the correct context.
HTTP 422 Unprocessable Entity is more semantically accurate than HTTP 404 Not Found when the issue is a missing reference inside a valid JSON payload.
The key distinction:
-
404 Not Found means the target resource (identified by the URL) does not exist. For example,
GET /sensors/NONEXISTENTreturning 404 correctly means "there is no sensor at this URL." -
422 Unprocessable Entity means the server received the request, the URL target does exist (
POST /sensorsis a valid endpoint), and the JSON payload is syntactically valid — but the server cannot process it due to semantic errors in the data.
When a client sends POST /sensors with {"roomId": "NONEXISTENT"}:
- The URL
/sensorsexists and accepts POST requests ✓ - The JSON body is well-formed and parseable ✓
- But the
roomIdvalue references a room that doesn't exist ✗
Using 404 here would be misleading because it would imply the /sensors endpoint itself doesn't exist. The 422 status code correctly communicates: "I understood your request and the syntax is fine, but I can't process it because the data contains invalid references."
This distinction helps client developers quickly identify whether the issue is with the URL they're calling (404) or with the data they're sending (422).
Exposing internal Java stack traces to external API consumers poses significant cybersecurity risks:
-
Technology Stack Disclosure: Stack traces reveal the programming language (Java), framework (JAX-RS/Jersey), and specific library versions. Attackers can use this to search for known CVEs (Common Vulnerabilities and Exposures) targeting those exact versions.
-
Internal Architecture Exposure: Package names (e.g.,
com.smartcampus.resource.SensorResource) reveal the application's internal class structure, naming conventions, and architectural patterns. This provides a roadmap for understanding how the application is organised. -
File System Path Leakage: Stack traces often include absolute file paths (e.g.,
/home/deploy/app/src/main/java/...), revealing the server's operating system, deployment structure, and potentially user account names. -
Business Logic Insights: Method names and call chains in the stack trace reveal business logic flow, error handling patterns, and potential weak points where input validation might be lacking.
-
SQL/Query Exposure: If database-related exceptions bubble up, the stack trace might contain partial SQL queries, table names, or connection strings — directly enabling SQL injection attacks.
-
Dependency Mapping: Third-party library classes appearing in the trace (e.g., specific Jackson or Hibernate versions) allow attackers to identify and exploit known vulnerabilities in those dependencies.
Our GenericExceptionMapper implementation mitigates all these risks by intercepting all unhandled Throwable instances, logging the full details server-side for debugging, and returning only a sanitized, generic error message to the client.
Using JAX-RS filters for cross-cutting concerns like logging is significantly advantageous over manually inserting Logger.info() statements inside every resource method:
-
DRY Principle (Don't Repeat Yourself): The logging logic is defined once in the filter class, not duplicated across dozens of resource methods. Changing the log format or adding new fields requires editing a single file.
-
Consistency: Every request and response is logged uniformly. With manual logging, developers might forget to add logging to new endpoints, use inconsistent formats, or log at different severity levels.
-
Separation of Concerns: Resource methods focus exclusively on business logic. Logging, authentication, CORS, compression, and other cross-cutting concerns are handled externally by filters, keeping the codebase clean and modular.
-
Automatic Coverage: The filter applies to all endpoints automatically, including any new resources added in the future. Manual logging requires explicit effort for each new endpoint.
-
Lifecycle Awareness: Filters have access to both the request and response contexts, allowing them to capture the complete request-response lifecycle (method, URI, headers, status code, timing) in a way that would require complex boilerplate if done manually.
-
Configurability: Filters can be enabled/disabled, prioritised (using
@Priority), or scoped to specific resources using@NameBindingannotations, providing fine-grained control without modifying business logic.