The six architectural principles
For an API to be truly RESTful, it has to meet a series of constraints. In chapter 5 of his dissertation, Roy Fielding describes six of them, and each has a concrete effect on scalability, maintainability and operations:
- Client–server separation: frontend and backend are decoupled and evolve independently. Your web frontend, your iOS app and the partner portal use the same API, without the backend needing to know who is asking.
- Statelessness: every request contains all the information the server needs to process it. The server stores no session state between two requests. This is the key to horizontal scaling: you can put any number of server instances behind a load balancer, because no instance needs to know what happened before. That’s exactly what you need when your shop has to handle a multiple of its usual load on Black Friday.
- Cacheability: responses use headers such as
Cache-Control and ETag to indicate whether and for how long they may be cached. Good caching takes load off the database and delivers frequently requested data such as product catalogues and categories quickly.
- Uniform interface: according to Fielding, the central feature that distinguishes REST from other architectural styles. It comprises four rules: resources are identified via URIs and manipulated via representations such as JSON, messages are self-descriptive, and links in the responses drive the further flow (Hypermedia as the Engine of Application State, HATEOAS for short).
- Layered system: proxies, gateways or caches may sit between client and server without the client noticing. This is exactly where an API gateway comes in.
- Code on demand (optional): the server may send executable code to the client, such as JavaScript. This is the only optional constraint.
In reality, many “REST APIs” do not strictly meet all six constraints. In everyday practice, a pragmatic “RESTful enough” applies. That’s fine as long as you make the trade-offs consciously. If you do without cacheability, for instance, you should have a good reason.
REST API or RESTful API? The Richardson Maturity Model
In everyday use, “REST API” and “RESTful API” are used synonymously. How RESTful an interface actually is can be classified with Leonard Richardson’s maturity model, which Martin Fowler described in 2010:
- Level 0: HTTP serves only as a transport. All calls go to a single endpoint, as with classic RPC.
- Level 1: There are individual resources with their own URLs, but the actions usually still run via a single HTTP method.
- Level 2: Methods and status codes are used as HTTP intends: GET reads, POST creates, 404 reports a missing resource.
- Level 3: Hypermedia controls (HATEOAS): responses contain links to the next possible actions, for example from an order to its cancellation.
Fielding himself is strict here: for him, an API whose flow is not driven by hypertext is not a REST API. Fowler sums up that, for Fielding, level 3 is a precondition of REST. If you don’t provide links to follow-up actions in your responses, you stay at level 2 by this reading. In practice, what matters most is that URLs, methods and status codes are consistent.
Using HTTP status codes correctly
A good REST API reports the result via the HTTP status code and not via a home-made field in the response body. Monitoring tools, load balancers, caches and client libraries evaluate the status code automatically. If you ignore it, you throw away much of the infrastructure the web already provides. The most important groups:
If you answer a failed login with 200 OK and {"error": true}, you break the uniform interface principle and make life unnecessarily hard for clients, monitoring and caches. Just as important: 429 Too Many Requests signals rate limiting cleanly, so that a well-built client reacts with backoff and doesn’t keep flooding the server.
REST API example 1: the Shopware Store API
Shopware 6 comes with two HTTP APIs that work with resource URLs and JSON. The Admin API at /api is intended for backend integrations, imports and data synchronisation and authenticates via OAuth 2.0 with an integration’s credentials. The Store API at /store-api serves customer frontends such as headless storefronts and apps. It expects a sales channel’s access key in the header sw-access-key (Shopware documentation).
A typical read request fetches products. Search parameters such as limit, filter or sort are sent as JSON in the request body; includes defines which fields come back:
POST /store-api/product HTTP/1.1
Host: shop.example.de
sw-access-key: SWSCXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json
Accept: application/json
{
"limit": 2,
"includes": {
"product": ["id", "productNumber", "name", "calculatedPrice"],
"calculated_price": ["unitPrice", "totalPrice"]
}
}
The response, shortened and with sample data:
HTTP/1.1 200 OK
Content-Type: application/json
{
"elements": [
{
"id": "0192a4c1e5f87d3b9a6e2f4c8d1b7e30",
"productNumber": "SW-4711",
"name": "Work Glove Pro",
"calculatedPrice": { "unitPrice": 12.9, "totalPrice": 12.9 },
"apiAlias": "product"
},
{
"id": "0192a4c1e5f87d3b9a6e2f4c8d1b7e31",
"productNumber": "SW-4712",
"name": "Safety Glasses Clear",
"calculatedPrice": { "unitPrice": 8.5, "totalPrice": 8.5 },
"apiAlias": "product"
}
],
"page": 1,
"limit": 2,
"total": 2,
"apiAlias": "dal_entity_search_result"
}
Three things can be read from this. First, the resource is in the URL (/store-api/product), the access key in the header. Second, includes limits the response to the fields the frontend really needs, which takes away part of REST’s over-fetching. Third, errors come as status codes: without sw-access-key, the Store API responds with 401 Unauthorized, and an unknown sort field returns 400 Bad Request with the error code FRAMEWORK__UNMAPPED_FIELD.
A detail that often surprises people in integrations: for /store-api/product, Shopware does not determine the total number of hits without the parameter total-count-mode; total then only counts the entries returned. For pagination with page numbers, you set "total-count-mode": 1 there. The product listing of a category (/store-api/product-listing/{categoryId}) returns the total count by itself. How the cart, customer context and checkout run via the Store API is explained in the glossary entry on the Store API.
Reading via POST? Search criteria and HTTP caching
At first glance, a read request via POST looks like a breach of REST. Shopware justifies it with the search criteria: filters, sort orders and aggregations quickly become too large for a URL. The price for this is caching. Under RFC 9110, responses to POST are cacheable only under narrow conditions, and Shopware marks non-cacheable Store API responses with Cache-Control: no-cache, private.
With version 6.7.6 (January 2026), Shopware added GET variants with HTTP caching for read routes such as product detail, product listing, search and search suggestions (release notes). Complex criteria are moved into the URL as a compressed query parameter _criteria. Shopware explicitly calls this a transitional solution until the HTTP method QUERY is standardised. According to the release notes, the configurable caching policies are still experimental and depend on the feature flag CACHE_REWORK.
The guideline from the associated architecture decision record is, in essence: GET for everything that changes nothing and returns no sensitive data. That’s a good rule of thumb for your own REST API, too.
In shop integrations, both APIs often work together: the ERP writes stock levels and prices via the Admin API, and the frontend reads them via the Store API. In between, authentication, error cases and retries have to be handled cleanly. You can see what such integrations look like on the pages on Shopware interfaces and on API integration with third-party providers.
REST API example 2: a controller in NestJS
What does the server side of a REST API look like? NestJS is a framework for server-side Node.js applications, built with TypeScript. It expresses resources, methods and validation via decorators. The following example models an order resource. First the data objects (DTOs), which define what a client may send:
// File: order.dto.ts
import {
IsDateString,
IsInt,
IsOptional,
IsString,
Min,
} from 'class-validator';
export class CreateOrderDto {
@IsString()
customerNumber!: string;
@IsString()
sku!: string;
@IsInt()
@Min(1)
quantity!: number;
}
export class UpdateOrderDto {
@IsOptional()
@IsDateString()
deliveryDate?: string;
}
Then the controller. Each method corresponds to a combination of HTTP method and path:
// File: orders.controller.ts
import {
Body,
Controller,
DefaultValuePipe,
Delete,
Get,
HttpCode,
NotFoundException,
Param,
ParseIntPipe,
ParseUUIDPipe,
Patch,
Post,
Query,
} from '@nestjs/common';
import { CreateOrderDto, UpdateOrderDto } from './order.dto';
import { OrdersService } from './orders.service';
@Controller('v1/orders') // resource in the plural, version in the path
export class OrdersController {
constructor(private readonly orders: OrdersService) {}
// GET /v1/orders?page=1&limit=20 → 200
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe)
page: number,
@Query('limit', new DefaultValuePipe(20), ParseIntPipe)
limit: number,
) {
return this.orders.findAll(page, Math.min(limit, 100));
}
// GET /v1/orders/:id → 200, 404, or 400 for an invalid ID
@Get(':id')
async findOne(@Param('id', ParseUUIDPipe) id: string) {
const order = await this.orders.findOne(id);
if (!order) throw new NotFoundException(`Order ${id} not found`);
return order;
}
// POST /v1/orders → 201 Created (Nest default for POST)
@Post()
create(@Body() dto: CreateOrderDto) {
return this.orders.create(dto);
}
// PATCH /v1/orders/:id → 200, changes only the fields sent
@Patch(':id')
async update(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateOrderDto,
) {
const order = await this.orders.update(id, dto);
if (!order) throw new NotFoundException(`Order ${id} not found`);
return order;
}
// DELETE /v1/orders/:id → 204 No Content
@Delete(':id')
@HttpCode(204)
async remove(@Param('id', ParseUUIDPipe) id: string) {
await this.orders.remove(id);
}
}
So that the decorators on the DTOs take effect, use main.ts to enable the ValidationPipe globally. For this, the project needs the packages class-validator and class-transformer:
app.useGlobalPipes(
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }),
);
whitelist strips fields that are not declared in the DTO. With forbidNonWhitelisted, the API instead rejects such requests with 400 (NestJS documentation). This means, for example, that a client can’t send along a price that the backend isn’t supposed to accept at all. The database logic sits in the OrdersService, which isn’t shown here.
There are two design decisions in this example. The list caps limit at 100 so that no client pulls the entire table with a single call. And a second DELETE returns 204 again, because the target state “order does not exist” has already been reached. 404 would be just as defensible, because RFC 9110 defines idempotency via the intended effect on the server. What matters is that your API’s behaviour is documented and consistent.
Authentication and permissions have been deliberately left out. In a real project, a guard checks whether the requesting customer is allowed to see the order at all. Without this check, anyone with a valid ID can retrieve other people’s orders, a classic case of Broken Access Control.
From the same decorators, the package @nestjs/swagger can generate an OpenAPI specification (NestJS documentation on OpenAPI). For the DTO fields to appear in the schema, they additionally need @ApiProperty() or the Swagger CLI plugin, which derives this information from the types and the class-validator decorators. Documentation and code thus stay in sync, and the frontend team can generate typed clients from it.
Other well-known REST APIs and OpenAPI
The Stripe API for payment processing is considered a prime example of clean REST design: clear resources such as /v1/customers or /v1/payment_intents, consistent status codes, a well-thought-out pagination model and very good documentation. The GitHub REST API is also a widely used, well-documented interface that you can use as a guide for your own design.
For the formal description of a REST API, the OpenAPI Specification (formerly Swagger) has become established. It describes endpoints, parameters and data models in a machine-readable YAML or JSON file. From this, documentation, client SDKs in various languages and test stubs can be generated automatically. You specify your interface cleanly once and then save manual work in many places. In our enterprise projects, the OpenAPI Specification is therefore often the central contract between the backend and frontend teams.
Versioning and backwards compatibility
A production API lives for years and is used by systems that you can’t all update at the same time. That’s why a versioning concept belongs in it from the start. Common practice is the version in the path (/v1/, /v2/, as in the NestJS example) or in a header. The golden rule: never silently reinterpret or remove existing fields. Adding new fields is almost always backwards compatible; removing or renaming never is. If you ignore this, you break partner integrations without warning.
REST vs. GraphQL: which style fits when
REST is the right choice if your interface is clearly resource-oriented, you need broad interoperability and good caching, and you serve many different clients: web, mobile and third-party systems. REST reaches its limits when clients need very different, deeply nested data. Then over-fetching (too much data per response) and under-fetching (several round trips for one view) lead to inefficiency, especially on mobile connections. For such cases, GraphQL is often the better alternative.
The information on GraphQL follows the recommendations of graphql.org on serving over HTTP. Shopify shows that the choice also concerns platform providers: there, the REST Admin API has been a legacy API since 1 October 2024, and since 1 April 2025 new public apps have had to use the GraphQL Admin API exclusively (Shopify documentation). Shopware, by contrast, has expanded its Store API with GET routes and HTTP caching.
In microservice landscapes, REST is often used for synchronous service-to-service communication, complemented by event-driven patterns such as webhooks for asynchronous notifications. In practice, the question is rarely “REST or something else?” but rather “which style for which use case?”. You’ll find a more detailed decision guide on REST, GraphQL and gRPC in the article on enterprise backend architecture.
A compact summary of the basic REST terms is available in the glossary of the MDN Web Docs.
Authentication and security
As soon as your REST API does more than deliver public data, you need a concept for authentication and authorisation. Common approaches are API keys for server-to-server communication, OAuth 2.0 with bearer tokens for third-party access, and JSON Web Tokens (JWT). JWTs suit the stateless nature of REST because they carry the necessary claims themselves.
In Shopware, the Admin API requires OAuth 2.0. The Store API’s sw-access-key, on the other hand, only assigns a request to a sales channel and is not a login: according to Shopware, the Store API is publicly accessible, and a login is only needed for customer-specific endpoints.
A production API belongs exclusively behind TLS (HTTPS), supplemented by rate limiting, input validation as in the NestJS example and a well-thought-out permission model. Especially in a B2B environment, interfaces often transport sensitive order, price or customer data. Security therefore belongs in the design from day one. If you postpone it, you set yourself up for an expensive retrofit project. The article on the OWASP Top 10 for Shopware and NestJS projects shows which risks specifically occur in such stacks.
Frequently asked questions about REST APIs
What does REST stand for?
REST stands for Representational State Transfer and describes an architectural style for distributed systems that Roy Fielding defined in his doctoral dissertation in 2000.
What is the difference between a REST API and a RESTful API?
In everyday use, none; both terms mean the same thing. Strictly speaking, an API is only RESTful if it meets all REST constraints, including hypermedia control (level 3 in the Richardson Maturity Model). Many APIs that call themselves RESTful do without this part.
What is an example of a REST API?
The Shopware Store API: a POST to /store-api/product with the header sw-access-key returns products as JSON. Other well-known examples are the APIs of Stripe and GitHub. The NestJS controller above shows what the server side looks like.
Is the Shopware API a REST API?
Shopware itself speaks of HTTP-based APIs with JSON. Both work with resource URLs, HTTP methods and status codes and are therefore commonly referred to as REST APIs: the Admin API for integrations, the Store API for frontends and apps. One special feature: many read routes of the Store API accept search criteria via POST. Since version 6.7.6, key read routes have also offered GET with HTTP caching.
Is REST the same as HTTP?
No. HTTP is the transport protocol that REST almost always uses in practice. REST itself is an architectural style that could theoretically also work with other protocols, but in reality is practically always built on HTTP.
What is the difference between REST and SOAP?
SOAP is a strict, XML-based protocol with a fixed message format and its own standard stack. REST is more lightweight, mostly uses JSON and relies on the native HTTP mechanisms. Today REST is far more widespread on the web; SOAP is still found in older enterprise and public-sector interfaces.
What does stateless mean in REST?
The server does not store any session state between two requests. Each request must bring all the necessary information with it, such as an auth token. This enables horizontal scaling across many server instances.
Do I necessarily need JSON for a REST API?
No, but JSON is the usual format. REST is format-agnostic: via the Accept header, a client can also request XML or other representations (content negotiation).