From Servlets to REST: Evolving Endpoints. Servlets from a practitioner’s perspective with timeless lessons.
\n\n\n\nContext
\n\n\n\nWeb teams keep asking the same question in standups and hallway chats. Should we keep pushing everything through Servlets or lean into REST style endpoints with cleaner URLs and plain HTTP? With Tomcat 6 everywhere, Servlet 3.0 on the horizon, Jersey and JAX RS gaining real traction, and JSON all over the place thanks to AJAX and mobile work, the timing feels right to revisit the core ideas. Oracle buying Sun is still playing out, but regardless of where Java branding lands, the way we shape endpoints will outlast any logo on a box. Let�s strip it down to first principles and field notes.
\n\n\n\nDefinitions
\n\n\n\n- \n
- Servlet: A Java class that handles HTTP requests inside a container like Tomcat or Jetty through
doGet,doPost, and friends. You own the request flow, the headers, and the output stream. \n - REST style: Model your service as resources with representations. Use HTTP methods with meaning. GET fetches, POST creates or triggers, PUT replaces, DELETE removes. The server stays stateless between calls. URLs are nouns, not verbs. \n
- Resource: A thing with an address. Example:
/orders/42. It can have multiple representations, like JSON and XML. \n - Idempotent: Same call, many times, same result on the server. GET and PUT should be this. POST is not. \n
- JAX RS: Java API for REST style services with annotations like
@Path,@GET,@Produces. Jersey is the reference implementation. \n
Examples
\n\n\n\nFirst, the classic Servlet. It is simple, explicit, and puts everything in your hands.
\n\n\n\npublic class OrderServlet extends HttpServlet {\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {\n String path = req.getPathInfo(); // e.g. /42\n String id = path != null ? path.substring(1) : null;\n\n if (id == null || id.isEmpty()) {\n resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);\n resp.setContentType("application/json");\n resp.getWriter().write("{\\"error\\":\\"missing id\\"}");\n return;\n }\n\n Order order = OrderRepository.find(id);\n if (order == null) {\n resp.setStatus(HttpServletResponse.SC_NOT_FOUND);\n resp.setContentType("application/json");\n resp.getWriter().write("{\\"error\\":\\"order not found\\"}");\n return;\n }\n\n resp.setStatus(HttpServletResponse.SC_OK);\n resp.setContentType("application/json");\n resp.getWriter().write(toJson(order));\n }\n}\n\n\n\nAnd a simple mapping in web.xml to catch paths like /orders/* inside a Servlet container.
<servlet>\n <servlet-name>orders</servlet-name>\n <servlet-class>com.acme.OrderServlet</servlet-class>\n</servlet>\n<servlet-mapping>\n <servlet-name>orders</servlet-name>\n <url-pattern>/orders/*</url-pattern>\n</servlet-mapping>\n\n\n\nNow, the JAX RS flavor with Jersey. Same intent, less plumbing, stronger HTTP semantics right in the annotations.
\n\n\n\nimport javax.ws.rs.*;\nimport javax.ws.rs.core.*;\n\n@Path("/orders")\n@Produces(MediaType.APPLICATION_JSON)\npublic class OrderResource {\n\n @GET\n @Path("/{id}")\n public Response get(@PathParam("id") String id) {\n Order o = OrderRepository.find(id);\n if (o == null) {\n return Response.status(Response.Status.NOT_FOUND)\n .entity("{\\"error\\":\\"order not found\\"}").build();\n }\n return Response.ok(toJson(o)).build();\n }\n\n @POST\n @Consumes(MediaType.APPLICATION_JSON)\n public Response create(String body, @Context UriInfo uri) {\n Order created = OrderRepository.save(fromJson(body));\n UriBuilder b = uri.getAbsolutePathBuilder().path(created.getId());\n return Response.created(b.build())\n .entity(toJson(created)).build();\n }\n}\n\n\n\nWith a quick curl run you can see the flow.
\n\n\n\n# create\ncurl -i -H "Content-Type: application/json" \\\n -d '{"sku":"ABC","qty":2}' \\\n http://localhost:8080/api/orders\n\n# fetch\ncurl -i http://localhost:8080/api/orders/42\n\n\n\nThe big win here is that URLs map to resources and HTTP status codes carry meaning. 201 Created with a Location header points to the new thing. 404 Not Found is not a guess. You can still do this by hand in a Servlet. The annotations just make the happy path clearer and keep boilerplate low.
\n\n\n\nCounterexamples
\n\n\n\n- \n
- One big action that does many things at once. Think of a monthly billing sweep that touches a lot of records and triggers emails. For that you may want a POST to a controller style endpoint or even a scheduled job, not a neat resource URL that pretends it is a single thing. \n
- When you must keep a server side session. Pure REST style wants no server memory of a client between calls. If you have to keep carts or wizards on the server for a while, you can still use clean URLs, just be honest about the session and its limits. \n
- Very chatty flows. If the client needs ten calls to build one screen, you may feel the lag. Add a coarse read endpoint that returns a view model to cut the number of round trips. \n
- Versioning drama. Breaking a popular endpoint is rough. You can version in the URL like
/v2/ordersor use a custom header. Either way, plan the deprecation story from day one. \n - File uploads and downloads. They work great in both worlds. Keep the URL simple, set Content Type right, and stream. Do not overthink it. \n
Decision rubric
\n\n\n\nWhen picking between a raw Servlet, JAX RS or a controller in Spring MVC, start with these points.
\n\n\n\n- \n
- Do you need explicit control over the stream? Choose a Servlet when you care about bytes, custom headers, and tight response control. Great for streaming, large downloads, or quirky clients. \n
- Do your URLs name things plainly? If you can say the resource as a noun and the verb is a known HTTP method, lean into REST style with JAX RS or Spring MVC with annotations. \n
- Are your reads cache friendly? If standard caching fits, map reads to GET, use ETag or Last Modified, and return 304 when nothing changed. That is free speed. \n
- Is the call safe to repeat? Use PUT for full updates when retry can happen. Keep POST for create or actions that are not repeat safe. \n
- Do you need strong typing at the boundary? If you must stick with XML Schema and SOAP because of a partner contract, do not fight it. You can still put a light REST style facade in front for your own clients later. \n
- Will you support many formats? Plan for Content Negotiation. Accept and produce JSON and XML at least. Keep the default simple. Clear docs help more than fancy tricks. \n
- Are teams split across stacks? If some teams live in Ruby or Python, a clean REST style contract with good examples keeps everyone moving without sharing a Java library. \n
Lesson learned
\n\n\n\nPure Servlets teach you the wire. That knowledge pays off every day. When a cache misbehaves or a proxy rewrites headers, you will spot it because you know what a clean HTTP exchange looks like. At the same time, writing everything by hand slows teams and invites tiny mistakes. An annotation based stack like JAX RS or Spring MVC keeps the intent front and center. The code shows the contract. The libraries push you toward correct status codes and sane content types.
\n\n\n\nWhat has aged well is not a brand or a framework. It is the idea that URLs name resources, HTTP methods carry meaning, and representations travel light. Keep nouns in the path, let verbs live in the method, return clear codes, and document with copy paste friendly examples. If a client can call your service with curl without guessing, you are doing it right.
\n\n\n\nSo here is a simple plan for the next sprint. For new endpoints that map cleanly to things, go with REST style using JAX RS or the annotation side of Spring MVC. Keep a few raw Servlets for the tricky corners like file moves, stream heavy work, or custom auth. Write doc pages that show the URL, method, sample request, sample response, and status codes. Add one sentence that explains what happens if something goes wrong. Ship it, watch the logs, tweak the contract early while the blast radius is small.
\n\n\n\nTools come and go. The HTTP contract and the resource mindset keep paying compound interest. The day you drop a reverse proxy in front, or move a service to a new host, or let a mobile app hit the same backend, your careful URL design and honest status codes will save the day. That is the part that endures.
\n\n\n\nTimeless takeaway: pick the simplest contract that fits the work. Start with resources, keep methods aligned with their meaning, and fall back to a Servlet only when you truly need the extra control. You will ship faster, debug less, and your endpoints will read like a story instead of a maze.
\n\n\n\n\n