DEVESSENTIALS

HTTP 201 Created

201 tells the client that something new now exists on the server. Per RFC 9110, the main created resource is identified by the Location header if there is one, and otherwise by the request URL. So a client can store the new URL without parsing the body, which a bare 200 doesn't allow.

2xx · SuccessRFC 9110 §15.3.2

201 Created

The request succeeded and created one or more new resources.

Who fixes it: Nobody (success). A success code. The things to get right are on the server: the Location header, the body, and not creating duplicates on retries.

What 201 means

It's usually the answer to POST /collection. A PUT /items/42 that creates an item that didn't exist yet should also return 201, and 200 or 204 if it replaced an existing one.

The body is up to you. Most APIs return the created resource, including server-generated fields such as id, createdAt and defaults, so the client doesn't need a follow-up GET.

Source: RFC 9110 §15.3.2.

When to return 201, and when not to

  • POST /users inserted a new row: return 201 with Location: /users/123.
  • PUT /files/report.pdf uploaded a file to a new path: 201. Replacing an existing file is 200 or 204.
  • The work was only queued (a video transcode, a bulk import): it's not created yet. Return 202 Accepted with a URL the client can poll.
  • The POST was an action that created nothing (search, login, sending an email): return 200.
  • The resource already existed (duplicate unique key): return 409 Conflict, or point to the existing resource if your API is idempotent by design.

How to return a 201

Express

res.location() sets the Location header. Relative URLs are allowed; RFC 9110 resolves them against the request URL.

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);
});

Next.js Route Handler

Response.json() takes the status and headers in its second argument.

// app/api/users/route.ts
export async function POST(request: Request) {
  const input = await request.json();
  const user = await createUser(input);
  return Response.json(user, {
    status: 201,
    headers: { Location: `/api/users/${user.id}` },
  });
}

Make retries safe with an idempotency key

If a client times out and retries a POST, you may create two resources. The Idempotency-Key convention (popularised by Stripe, now an IETF draft) fixes this: store the key with the result and replay the original 201 for repeat keys.

curl -i https://api.example.com/users \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 6f1c2a4e-9b1d-4c1e-8a57-1f0e2b7d9c11' \
  -d '{"email":"ada@example.com"}'

HTTP/1.1 201 Created
Location: /users/123
Content-Type: application/json

Headers that matter with 201

Location
The URL of the new resource. It can be relative, e.g. /users/123.
Access-Control-Expose-Headers
Location isn't a CORS-safelisted response header, so a cross-origin fetch() can't read it unless the server sends Access-Control-Expose-Headers: Location.
ETag / Last-Modified
If sent with a 201, they describe the newly created resource, so the client can make a conditional update straight away.

201 vs similar status codes

  • 200 OK: Generic success. Fine for actions that return a result, but loses the "something was created" signal.
  • 202 Accepted: The request was accepted but the resource doesn't exist yet. Return a status URL instead of the resource.
  • 409 Conflict: The resource you tried to create already exists, or clashes with the current state.

Related status codes

Looking for another code? Search the full HTTP status codes list, or read which code your API should return in the REST API status codes guide.

Learn more

Browse all developer guides →

Frequently Asked Questions

Does a 201 response need a body?▾

No, the body is optional. Many APIs return the created resource so clients get server-generated fields such as the id without a second request. The Location header is the standard way to say where the new resource lives.

Is it wrong to return 200 instead of 201 after creating something?▾

It works, and plenty of APIs do it, but it hides the fact that a resource was created. 201 with a Location header lets generic clients, SDK generators and tests understand the outcome from the status line alone.

Why can't my frontend read the Location header of a 201?▾

For cross-origin requests the browser only exposes CORS-safelisted response headers to JavaScript, and Location isn't one of them. Add Access-Control-Expose-Headers: Location on the server, or include the new id or URL in the JSON body.