Backend EngineeringREST APIBest PracticesHTTP

REST API Best Practices for Backend Engineers

Satyam Parmar
January 18, 2025
7 min read

REST API Best Practices for Backend Engineers

Building robust, scalable REST APIs requires following established conventions and best practices. This guide covers everything you need to know to create production-ready APIs.

URL Design

Use Nouns, Not Verbs

# Good
GET /api/users
POST /api/users
PUT /api/users/123
DELETE /api/users/123

# Bad
GET /api/getUsers
POST /api/createUser
POST /api/updateUser
POST /api/deleteUser

Use Plural Nouns

# Good
GET /api/users
GET /api/orders
GET /api/products

# Bad
GET /api/user
GET /api/order
GET /api/product

Use Hierarchical Structure

# Good
GET /api/users/123/orders
GET /api/users/123/orders/456

# Bad
GET /api/userOrders?userId=123
GET /api/orderDetails?orderId=456

HTTP Status Codes

Success Codes

// 200 OK - Successful GET, PUT, PATCH
app.get('/api/users/:id', (req, res) => {
  const user = getUserById(req.params.id);
  res.status(200).json(user);
});

// 201 Created - Successful POST
app.post('/api/users', (req, res) => {
  const user = createUser(req.body);
  res.status(201).json(user);
});

// 204 No Content - Successful DELETE
app.delete('/api/users/:id', (req, res) => {
  deleteUser(req.params.id);
  res.status(204).send();
});

Java/Spring Boot:

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    // 200 OK - Successful GET, PUT, PATCH
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    // 201 Created - Successful POST
    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        User createdUser = userService.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
    }
    
    // 204 No Content - Successful DELETE
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteById(id);
        return ResponseEntity.noContent().build();
    }
}

Error Codes

// 400 Bad Request - Invalid input
app.post('/api/users', (req, res) => {
  if (!req.body.email) {
    return res.status(400).json({
      error: 'Email is required'
    });
  }
});

// 401 Unauthorized - Authentication required
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json(req.user);
});

// 403 Forbidden - Access denied
app.delete('/api/users/:id', (req, res) => {
  if (req.user.id !== req.params.id) {
    return res.status(403).json({
      error: 'Access denied'
    });
  }
});

// 404 Not Found - Resource doesn't exist
app.get('/api/users/:id', (req, res) => {
  const user = getUserById(req.params.id);
  if (!user) {
    return res.status(404).json({
      error: 'User not found'
    });
  }
  res.json(user);
});

// 422 Unprocessable Entity - Validation failed
app.post('/api/users', (req, res) => {
  const errors = validateUser(req.body);
  if (errors.length > 0) {
    return res.status(422).json({
      error: 'Validation failed',
      details: errors
    });
  }
});

// 500 Internal Server Error - Server error
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    error: 'Internal server error'
  });
});

Java/Spring Boot:

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    // 400 Bad Request - Invalid input
    @PostMapping
    public ResponseEntity<?> createUser(@Valid @RequestBody User user, BindingResult result) {
        if (result.hasErrors()) {
            return ResponseEntity.badRequest()
                .body(new ErrorResponse("Validation failed", result.getFieldErrors()));
        }
        User createdUser = userService.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
    }
    
    // 401 Unauthorized - Authentication required
    @GetMapping("/profile")
    @PreAuthorize("isAuthenticated()")
    public ResponseEntity<User> getProfile(Authentication auth) {
        User user = userService.findByEmail(auth.getName());
        return ResponseEntity.ok(user);
    }
    
    // 403 Forbidden - Access denied
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or @userService.isOwner(#id, authentication.name)")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteById(id);
        return ResponseEntity.noContent().build();
    }
    
    // 404 Not Found - Resource doesn't exist
    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return userService.findById(id)
            .map(user -> ResponseEntity.ok(user))
            .orElse(ResponseEntity.notFound().build());
    }
    
    // 422 Unprocessable Entity - Validation failed
    @PostMapping
    public ResponseEntity<?> createUserWithValidation(@Valid @RequestBody User user, BindingResult result) {
        if (result.hasErrors()) {
            return ResponseEntity.unprocessableEntity()
                .body(new ValidationErrorResponse("Validation failed", result.getFieldErrors()));
        }
        User createdUser = userService.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
    }
}

// Global Exception Handler
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleInternalServerError(Exception ex) {
        log.error("Internal server error", ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("Internal server error", null));
    }
}

Request/Response Format

Consistent Response Structure

// Success Response
{
  "success": true,
  "data": {
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com"
  },
  "message": "User created successfully"
}

// Error Response
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email is required",
    "details": [
      {
        "field": "email",
        "message": "Email is required"
      }
    ]
  }
}

Pagination

// Request
GET /api/users?page=1&limit=10&sort=name&order=asc

// Response
{
  "success": true,
  "data": [
    // ... users array
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 100,
    "pages": 10,
    "hasNext": true,
    "hasPrev": false
  }
}

Authentication & Authorization

JWT Implementation

const jwt = require('jsonwebtoken');

// Generate token
function generateToken(user) {
  return jwt.sign(
    { 
      id: user.id, 
      email: user.email,
      role: user.role 
    },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );
}

// Verify token
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid token' });
    }
    req.user = user;
    next();
  });
}

API Versioning

URL Versioning

// Version 1
app.use('/api/v1/users', userRoutesV1);

// Version 2
app.use('/api/v2/users', userRoutesV2);

Header Versioning

app.use((req, res, next) => {
  const version = req.headers['api-version'] || '1.0';
  req.apiVersion = version;
  next();
});

Rate Limiting

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: {
    error: 'Too many requests from this IP, please try again later.'
  }
});

app.use('/api/', limiter);

Input Validation

const { body, validationResult } = require('express-validator');

const validateUser = [
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 6 }),
  body('name').trim().isLength({ min: 1 }),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({
        error: 'Validation failed',
        details: errors.array()
      });
    }
    next();
  }
];

app.post('/api/users', validateUser, createUser);

Error Handling

class APIError extends Error {
  constructor(message, statusCode, code) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true;
  }
}

// Global error handler
app.use((err, req, res, next) => {
  if (err instanceof APIError) {
    return res.status(err.statusCode).json({
      success: false,
      error: {
        code: err.code,
        message: err.message
      }
    });
  }

  // Log unexpected errors
  console.error(err.stack);
  
  res.status(500).json({
    success: false,
    error: {
      code: 'INTERNAL_ERROR',
      message: 'Internal server error'
    }
  });
});

Performance Optimization

Caching

const redis = require('redis');
const client = redis.createClient();

async function getCachedUser(userId) {
  const cached = await client.get(`user:${userId}`);
  if (cached) {
    return JSON.parse(cached);
  }
  
  const user = await getUserFromDB(userId);
  await client.setex(`user:${userId}`, 3600, JSON.stringify(user));
  return user;
}

Compression

const compression = require('compression');
app.use(compression());

Documentation

OpenAPI/Swagger

const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'User API',
      version: '1.0.0',
      description: 'A simple User API'
    },
    servers: [
      {
        url: 'http://localhost:3000/api',
        description: 'Development server'
      }
    ]
  },
  apis: ['./routes/*.js']
};

const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

Testing

const request = require('supertest');
const app = require('../app');

describe('User API', () => {
  test('GET /api/users should return users', async () => {
    const response = await request(app)
      .get('/api/users')
      .expect(200);
    
    expect(response.body.success).toBe(true);
    expect(Array.isArray(response.body.data)).toBe(true);
  });

  test('POST /api/users should create user', async () => {
    const userData = {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'password123'
    };

    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .expect(201);
    
    expect(response.body.success).toBe(true);
    expect(response.body.data.email).toBe(userData.email);
  });
});

Conclusion

Following these REST API best practices will help you build robust, scalable, and maintainable APIs. Remember to:

  • Keep your APIs consistent
  • Use appropriate HTTP status codes
  • Implement proper error handling
  • Add authentication and authorization
  • Document your APIs
  • Test thoroughly
  • Monitor and optimize performance

These practices will make your APIs more reliable and easier to use for frontend developers and API consumers.

Related Articles

Home