@PostMapping

Maps HTTP POST requests to a handler method; use it to create resources and trigger actions.

Signature
@PostMapping(consumes = "application/json")
  • Added in Spring Boot Spring Framework 4.3

Parameters

ParameterTypeDescription
$valueoptionalString[]= {}Alias for path. One or more URI patterns such as "/{id}". Combined with any class-level @RequestMapping path.
$pathoptionalString[]= {}The URI patterns this method handles. Same as value; use whichever reads better.
$paramsoptionalString[]= {}Narrows the mapping by query parameters: "type", "!type" or "type=pdf".
$headersoptionalString[]= {}Narrows the mapping by request headers, for example "X-Feature=beta".
$consumesoptionalString[]= {}Media types the request body must have (checked against Content-Type). A mismatch gives 415.
$producesoptionalString[]= {}Media types the method can write (checked against Accept). No overlap gives 406.
$versionoptionalString= ""API version this mapping serves, such as "1.1" or "1.1+". Needs API versioning configured.
$nameoptionalString= ""A name for the mapping, used by MvcUriComponentsBuilder to build links. Rarely needed.

Return value

n/aShortcut for @RequestMapping(method = RequestMethod.POST). The method handles POST requests, usually reading a @RequestBody.

Errors

A body that is not valid JSON gives 400; a failed @Valid check gives 400 via MethodArgumentNotValidException; an unsupported Content-Type gives 415.

Changelog

  • PHP Spring Framework 7.0Added the version attribute for first-class API versioning.

@PostMapping is @RequestMapping(method = POST). POST is the HTTP method for creating a new resource in a collection (POST /api/customers) and for actions that do not fit the other verbs (POST /api/orders/42/refund). Unlike GET and PUT, POST is not idempotent: sending it twice may create two customers or two refunds.

Return the right status

By default a handler returns 200. For a create endpoint the correct answer is 201 Created with a Location header pointing at the new resource. Clients and API gateways use that header to find what was created. For work that runs later, return 202 Accepted with something the client can poll.

@PostMapping
ResponseEntity<Customer> create(@Valid @RequestBody CreateCustomer body) {
    Customer saved = service.create(body);
    URI location = ServletUriComponentsBuilder.fromCurrentRequest()
            .path("/{id}").buildAndExpand(saved.id()).toUri();
    return ResponseEntity.created(location).body(saved);
}

ServletUriComponentsBuilder.fromCurrentRequest() builds the URL from the incoming request, so it is correct on every host. Behind a proxy, set server.forward-headers-strategy so the scheme and host come from the forwarded headers rather than the internal address.

Duplicates and retries

Mobile clients, load balancers and users double-clicking a button all resend POSTs. For anything that costs money or sends email, accept an Idempotency-Key header and store the result against it, so a repeat returns the first response instead of doing the work again. Back the store with a unique database constraint, not just an in-memory map.

Mistakes people make

  • Binding the request straight to a JPA entity. A client can then set id, createdAt or role. Use a record that contains only the fields a client may send.
  • Forgetting @Valid. Constraints on the record do nothing until the parameter is annotated.
  • Returning 200 for creates. It works, but clients lose the Location header and generated tools describe the wrong status.

Example

Example · java
import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

record CreateCustomer(@NotBlank String name, @Email @NotBlank String email) {}

record Customer(long id, String name, String email) {}

@RestController
@RequestMapping("/api/customers")
public class CustomerController {

    private final Map<Long, Customer> store = new ConcurrentHashMap<>();
    private final AtomicLong ids = new AtomicLong();

    @PostMapping
    ResponseEntity<Customer> create(@Valid @RequestBody CreateCustomer request) {
        Customer saved = new Customer(ids.incrementAndGet(), request.name(), request.email());
        store.put(saved.id(), saved);

        // 201 Created with Location: http://host/api/customers/{id}
        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(saved.id())
                .toUri();
        return ResponseEntity.created(location).body(saved);
    }
}

When to use it

  • Registering a new customer from a sign-up form
  • Charging a card exactly once even when the mobile app retries
  • Starting a long report export and returning a job id

Frequently asked questions

Should a create endpoint return the created object?

Usually yes, together with 201 and a Location header. It saves the client a second request to read generated fields such as the id.

Why is my Location header showing an internal host name?

The app sees the proxy's internal request. Set server.forward-headers-strategy so Spring uses X-Forwarded-* or Forwarded headers when building URLs.

More examples

201 without a Location header

@ResponseStatus(CREATED) changes the status when the method returns normally. It is fine when the created thing has no URL of its own; otherwise prefer ResponseEntity.created(uri).

Example · java
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

record Feedback(String message) {}

record Ticket(String reference) {}

@RestController
public class FeedbackController {

    // No Location needed: the ticket is not addressable on its own.
    @PostMapping("/api/feedback")
    @ResponseStatus(HttpStatus.CREATED)
    Ticket submit(@RequestBody Feedback feedback) {
        return new Ticket("FB-" + Math.abs(feedback.message().hashCode()));
    }
}

Safe retries with an idempotency key

The client generates a key per logical operation and resends it on retry. computeIfAbsent returns the stored payment for a known key, so the charge happens once. A missing header gives 400 automatically.

Example · java
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

record PaymentRequest(String orderId, long amountInPence) {}

record Payment(String id, String orderId, long amountInPence) {}

@RestController
public class PaymentController {

    // In production this lives in a table with a unique constraint and an expiry.
    private final Map<String, Payment> byKey = new ConcurrentHashMap<>();

    @PostMapping("/api/payments")
    @ResponseStatus(HttpStatus.CREATED)
    Payment pay(@RequestHeader("Idempotency-Key") String key, @RequestBody PaymentRequest request) {
        // A retried request with the same key gets the original result, not a second charge.
        return byKey.computeIfAbsent(key, k ->
                new Payment("pay_" + k.hashCode(), request.orderId(), request.amountInPence()));
    }
}

An action endpoint returning 202

Actions that are not plain creates still use POST. 202 Accepted tells the client the work was queued; the body carries a job id it can poll.

Example · java
import java.util.Map;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ExportController {

    // An action, not a resource: start a long job and return 202 Accepted.
    @PostMapping("/api/reports/{id}/export")
    ResponseEntity<Map<String, String>> export(@PathVariable long id) {
        String jobId = "job-" + id;
        return ResponseEntity.accepted().body(Map.of("job", jobId, "status", "QUEUED"));
    }
}

Discussion

  • Be the first to comment on this lesson.