# 201 Created

> Resource successfully created. Learn when to use 201 Created, proper response format, and best practices for creation endpoints.

Source: https://howhttpworks.com/status-codes/201
Last reviewed: 2026-10-05

> **TL;DR:** 201 Created means the request succeeded and a new resource now exists. Return its URL in the `Location` header and, usually, a representation of it in the body.

## What it means

The HTTP exchanges on this page are illustrative. The first response creates user 42 and gives the client both its URL and its current representation.

```http
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 40

{"name":"Ada","email":"ada@example.com"}

HTTP/1.1 201 Created
Location: https://api.example.com/api/users/42
Content-Type: application/json
ETag: "v1"

{"id":42,"name":"Ada","email":"ada@example.com"}
```

RFC 9110 section 15.3.2: the new resource is identified by the `Location` header, or by the request URL if there is none. The resource must exist when the response is sent; if creation is queued, use 202 instead. `Location` may be relative. Resolve it against the request URL; an absolute URL must use the public scheme and host, especially behind a reverse proxy.

## What you see in your client

A search for “201 error code” usually leads to a success response. Check the status predicate in your application before changing the server: `status === 200` rejects valid creation responses.

- **Axios:** 201 resolves with `response.status === 201` under the default `validateStatus`. A custom predicate that accepts only 200 produces `AxiosError: Request failed with status code 201`. Restore the 200–299 range if the endpoint creates resources. See Axios's [defaults](https://github.com/axios/axios/blob/v1.x/lib/defaults/index.js) and [rejection message](https://github.com/axios/axios/blob/v1.x/lib/core/settle.js).
- **fetch:** the promise resolves and `response.ok` is `true`. Read `response.status` when your UI needs to distinguish creation from another successful result. These properties follow the [Fetch Standard](https://fetch.spec.whatwg.org/#dom-response-ok).
- **Python requests:** `response.raise_for_status()` returns normally; `response.ok` is `True`. Its [implementation](https://requests.readthedocs.io/en/latest/_modules/requests/models/#Response.raise_for_status) raises `HTTPError` for 4xx and 5xx, so there is no built-in `201 Client Error` message.
- **curl `-f`:** 201 passes the HTTP failure check. Use `-i` to see the headers; [curl's manual](https://curl.se/docs/manpage.html#-f) sets the failure threshold at 400.
- **.NET:** `EnsureSuccessStatusCode()` returns the response. The [success check](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs) accepts 200–299.
- **Spring:** `WebClient.retrieve()` uses `WebClientResponseException` for [4xx/5xx by default](https://docs.spring.io/spring-framework/reference/web/webflux-webclient/client-retrieve.html). `RestTemplate`'s [default error handler](https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/client/DefaultResponseErrorHandler.java) likewise leaves 201 on the normal response path.

Keep `Location` separate from redirect handling. A 201 identifies what you just created; it leaves the client on the original request rather than initiating a 301/302 navigation. Read the body when present, or issue a GET to the resource URL if you need its representation.

## When to use it

- POST that creates a resource at a server-chosen URL (`POST /users` gives `/users/42`).
- PUT to a URL that did not exist yet: 201 on create, 200 or 204 on replace. This is the standard "upsert" signal.
- Not for: updates (200/204), work that has not finished (202), or "already exists" (409, or 200 with the existing resource for idempotent creates).

## 201 vs 200, 202 and 204

| Code | Use when                                                              |
| ---- | --------------------------------------------------------------------- |
| 200  | Success; response describes the result of the request               |
| 201  | A new resource exists now; `Location` points to it                    |
| 202  | Accepted for processing, not complete (return a status URL)           |
| 204  | Success, no body (updates, deletes)                                   |

## Implementation

### Express

```javascript
app.post('/api/users', async (req, res) => {
  const user = await db.users.create(req.body)
  res.status(201).location(`/api/users/${user.id}`).json(user)
})
```

### Django REST framework

```python
class UserCreate(generics.CreateAPIView):
    serializer_class = UserSerializer
    # CreateModelMixin returns 201; it sets Location only if the serialized data has a 'url' key
```

### Go net/http

```go
w.Header().Set("Location", "/api/users/"+id)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
```

### Spring

```java
@PostMapping("/api/users")
ResponseEntity<User> create(@RequestBody User u) {
    User saved = service.save(u);
    URI location = URI.create("/api/users/" + saved.getId());
    return ResponseEntity.created(location).body(saved);
}
```

## Common causes by stack

**Express:** `res.status(201).location('/api/users/42').json(user)` explicitly selects creation; `res.json(user)` alone leaves the response's status at its existing value. Check the route's final response call when a successful insert returns the wrong code. The [response API](https://expressjs.com/en/5x/api/response/#res.status) documents `status`, `location` and `json`.

**FastAPI:** `@app.post('/users', status_code=201)` sets the response status. The [default is 200](https://fastapi.tiangolo.com/tutorial/response-status-code/), even when your Python function inserts a row. Set 201 on the path operation that completes creation; queueing work calls for [202](https://howhttpworks.com/status-codes/202).

**Django REST framework:** `CreateAPIView` uses `CreateModelMixin`, whose successful `create()` response is 201. Its [creation behavior](https://www.django-rest-framework.org/api-guide/generic-views/#createmodelmixin) sets `Location` when the representation contains a `url` key. If you need that header, inspect the serializer output rather than assuming every model serializer includes it.

**Spring MVC / Spring Boot:** `ResponseEntity.created(location).body(saved)` builds a 201 and sets `Location`. Check the controller return value when a saved entity arrives as 200: returning the entity alone skips the [created builder](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/ResponseEntity.html#created(java.net.URI)).

**ASP.NET Core:** `CreatedAtAction(nameof(GetById), new { id = product.Id }, product)` returns 201 with a generated resource URL. Microsoft's [controller return-type guide](https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types) shows the matching `GetById` route. Verify that route and its parameter names when the generated URL points somewhere unexpected.

## PUT, CalDAV and an empty body

A PUT that creates the resource at its target URL returns 201; replacing an existing representation uses 200 or 204. This distinction comes from [RFC 9110 section 9.3.4](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.4). For a calendar client uploading a new resource with PUT, inspect whether that URL already existed before interpreting 201 as a failure.

An empty 201 body is valid. Check `Content-Type` and the endpoint contract before calling a JSON decoder. A successful creation and a JSON parse failure are separate events: repair the decoder or return the promised JSON rather than retrying the write. The response can identify the resource through the request URL when `Location` is absent.

## Common mistakes

- **Missing `Location`.** Spring's [`RestTemplate.postForLocation`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html) returns this header's value. Supply it when the new resource lives at a different URL.
- **Treating 201 as retry protection.** A repeated POST can create another resource. If your API supports an idempotency key, retain the same key for retries of the same operation.
- **201 before the resource exists**, for example before an async job inserts the row. Use 202.
- **Returning 201 on a duplicate** that was silently merged. Return 409 or the existing resource with 200.
- **Caching sensitive creation responses.** 201 is not heuristically cacheable. Choose an explicit cache policy for the data you return; an `ETag` can support later `If-Match` updates.

## Reproduce with curl

```bash
curl -si -X POST https://api.example.com/api/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Ada","email":"ada@example.com"}'
```

Inspect the response status and `location:` line. The HTTP version depends on the connection. Use `-D - -o /dev/null` to print headers without the body, then filter the header you need:

```bash
curl -sS -D - -o /dev/null https://api.example.com/api/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Ada","email":"ada@example.com"}' \
  | grep -i '^location:'
```

Add `-w '%{redirect_url}\n'` to print the resolved `Location` URL. Curl [records that header even without following it](https://github.com/curl/curl/blob/master/lib/multi.c); the header command above shows the value as sent by the server.

## Related

- [200 OK](https://howhttpworks.com/status-codes/200)
- [202 Accepted](https://howhttpworks.com/status-codes/202)
- [204 No Content](https://howhttpworks.com/status-codes/204)
- [409 Conflict](https://howhttpworks.com/status-codes/409)
- [Location header](https://howhttpworks.com/headers/location)
- [POST method](https://howhttpworks.com/methods/post)

See also the comparison [200 vs 201 vs 204](https://howhttpworks.com/compare/200-vs-201-vs-204): choosing the right success code for create, read, update and delete.
