Skip to main content

REST APIs

REST (Representational State Transfer) is an architectural style for designing networked applications using HTTP protocols.

REST Principles

Architectural Constraints

Client-Server Architecture

  • Separation of concerns: Client handles UI, server handles data
  • Independence: Client and server can evolve independently
  • Scalability: Server can scale without affecting clients
  • Portability: Clients can be deployed on different platforms

Statelessness

"HTTP is a request-response protocol but imagine it as a conversation with no memory."

  • No session state: Each request contains all necessary information
  • Server simplicity: No need to maintain client state
  • Reliability: Easier to recover from failures
  • Scalability: Load balancers can distribute requests freely

Cacheability

  • Explicit caching: Responses must define cacheability
  • Performance: Reduce client-server interactions
  • Scalability: Reduce server load
  • Efficiency: Better network utilization

Uniform Interface

  • Standardization: Consistent interface across all resources
  • Simplicity: Easier to understand and use
  • Evolution: Independent evolution of components
  • Interoperability: Different clients can work with same API

Layered System

  • Hierarchical architecture: Multiple layers between client and server
  • Encapsulation: Each layer sees only adjacent layers
  • Load balancing: Multiple servers behind single interface
  • Security: Additional security layers

Code on Demand (Optional)

  • Server logic: Server can send executable code to client
  • Flexibility: Extend client functionality dynamically
  • Complexity: Adds implementation complexity
  • Security: Potential security risks

HTTP Methods and Status Codes

HTTP Methods

GET

  • Purpose: Retrieve resource representation
  • Idempotent: Multiple calls have same effect
  • Safe: Does not modify server state
  • Cacheable: Responses can be cached

POST

  • Purpose: Create new resource
  • Non-idempotent: Multiple calls create multiple resources
  • Not safe: Modifies server state
  • Not cacheable: Typically not cached

PUT

  • Purpose: Update or replace resource
  • Idempotent: Multiple calls have same effect
  • Not safe: Modifies server state
  • Not cacheable: Typically not cached

PATCH

  • Purpose: Partial update of resource
  • Non-idempotent: Depends on implementation
  • Not safe: Modifies server state
  • Not cacheable: Typically not cached

DELETE

  • Purpose: Delete resource
  • Idempotent: Multiple calls have same effect
  • Not safe: Modifies server state
  • Not cacheable: Typically not cached

HTTP Status Codes

2xx Success Codes

  • 200 OK: Request successful
  • 201 Created: Resource created successfully
  • 202 Accepted: Request accepted for processing
  • 204 No Content: Request successful, no content returned

3xx Redirection Codes

  • 301 Moved Permanently: Resource permanently moved
  • 302 Found: Resource temporarily moved
  • 304 Not Modified: Resource not modified (conditional GET)

4xx Client Error Codes

  • 400 Bad Request: Invalid request syntax
  • 401 Unauthorized: Authentication required
  • 403 Forbidden: Access denied
  • 404 Not Found: Resource not found
  • 409 Conflict: Request conflicts with current state

5xx Server Error Codes

  • 500 Internal Server Error: Unexpected server error
  • 502 Bad Gateway: Invalid response from upstream server
  • 503 Service Unavailable: Server temporarily unavailable
  • 504 Gateway Timeout: Upstream server timeout

Resource Design

Resource Identification

  • URI design: Use nouns, not verbs
  • Hierarchical structure: Reflect resource relationships
  • Plural nouns: Use plural form for collections
  • Consistent naming: Follow naming conventions

Good Examples:

GET /users          # Get all users
GET /users/123 # Get specific user
GET /users/123/orders # Get user's orders
POST /users # Create new user
PUT /users/123 # Update user
DELETE /users/123 # Delete user

Bad Examples:

GET /getAllUsers
POST /createUser
GET /users?id=123&action=delete

Resource Representations

  • JSON format: Standard for modern APIs
  • Consistent structure: Uniform response format
  • Links and relationships: HATEOAS principles
  • Metadata: Pagination, timestamps, version info

Response Structure Example:

{
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z"
},
"links": {
"self": "/users/123",
"orders": "/users/123/orders"
},
"meta": {
"version": "v1",
"timestamp": "2023-01-01T00:00:00Z"
}
}

API Design Patterns

Pagination

  • Limit/Offset: Simple but inefficient for large datasets
  • Cursor-based: More efficient for large datasets
  • Page-based: Fixed-size pages
  • Infinite scroll: Cursor-based with automatic loading

Limit/Offset Example:

GET /users?limit=20&offset=40

Cursor-based Example:

GET /users?limit=20&after=cursor123

Filtering and Sorting

  • Query parameters: Filter resources
  • Multiple filters: Combine multiple criteria
  • Sorting: Order results by specific fields
  • Default sorting: Predictable default order

Filtering Example:

GET /users?status=active&role=admin

Sorting Example:

GET /users?sort=created_at:desc,name:asc

Versioning

  • URL versioning: /v1/users, /v2/users
  • Header versioning: Accept: application/vnd.api+json;version=1
  • Query parameter: /users?version=1
  • Content negotiation: Different media types

Error Handling

  • Consistent format: Standardized error responses
  • Detailed information: Error codes, messages, details
  • HTTP status codes: Appropriate status codes
  • Validation errors: Field-specific error information

Error Response Example:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "age",
"message": "Age must be between 18 and 100"
}
]
}
}

Implementation Examples

Express.js REST API

const express = require('express');
const app = express();

app.use(express.json());

// GET /users
app.get('/users', async (req, res) => {
const { limit = 10, offset = 0 } = req.query;
const users = await User.find().limit(limit).skip(offset);
res.json({ data: users });
});

// GET /users/:id
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: { code: 'NOT_FOUND', message: 'User not found' }
});
}
res.json({ data: user });
});

// POST /users
app.post('/users', async (req, res) => {
try {
const user = await User.create(req.body);
res.status(201).json({ data: user });
} catch (error) {
res.status(400).json({
error: { code: 'VALIDATION_ERROR', message: error.message }
});
}
});

// PUT /users/:id
app.put('/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({
error: { code: 'NOT_FOUND', message: 'User not found' }
});
}
res.json({ data: user });
});

// DELETE /users/:id
app.delete('/users/:id', async (req, res) => {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({
error: { code: 'NOT_FOUND', message: 'User not found' }
});
}
res.status(204).send();
});

app.listen(3000, () => console.log('Server running on port 3000'));

Spring Boot REST API

@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserService userService;

@GetMapping
public ResponseEntity<ApiResponse<List<User>>> getUsers(
@RequestParam(defaultValue = "10") int limit,
@RequestParam(defaultValue = "0") int offset) {
List<User> users = userService.getUsers(limit, offset);
return ResponseEntity.ok(ApiResponse.success(users));
}

@GetMapping("/{id}")
public ResponseEntity<ApiResponse<User>> getUser(@PathVariable Long id) {
return userService.getUser(id)
.map(user -> ResponseEntity.ok(ApiResponse.success(user)))
.orElse(ResponseEntity.notFound().build());
}

@PostMapping
public ResponseEntity<ApiResponse<User>> createUser(@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(user));
}

@PutMapping("/{id}")
public ResponseEntity<ApiResponse<User>> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.updateUser(id, request)
.map(user -> ResponseEntity.ok(ApiResponse.success(user)))
.orElse(ResponseEntity.notFound().build());
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (userService.deleteUser(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
}

Best Practices

Design Principles

  • Consistency: Uniform interface and naming conventions
  • Simplicity: Easy to understand and use
  • Flexibility: Support for different client needs
  • Performance: Efficient resource utilization

Security Considerations

  • Authentication: Verify client identity
  • Authorization: Control access to resources
  • HTTPS: Encrypt communication
  • Input validation: Prevent injection attacks

Performance Optimization

  • Caching: Implement appropriate caching strategies
  • Compression: Reduce response size
  • Connection pooling: Reuse connections
  • Pagination: Limit response sizes

Key Takeaway: REST APIs provide a simple, scalable, and widely adopted approach to building web services that leverage HTTP's strengths while maintaining statelessness and cacheability.