REST API Design: 10 Best Practices Every Developer Needs
Tutorials

REST API Design: 10 Best Practices Every Developer Needs

13 min read
103 Views
Share:

Why API design matters more than you think

After building and consuming dozens of REST APIs over my career, I can tell you that the difference between a good API and a bad one is not the technology behind it. It is the design. A well-designed API is intuitive, consistent, and predictable. A poorly designed one causes frustration, bugs, and wasted hours for every developer who uses it.

These 10 best practices are not theoretical. They come from real experience building APIs consumed by mobile apps, frontend teams, and third-party integrators. Following them will make your APIs easier to build, easier to use, and easier to maintain.

1. Use nouns for resources, not verbs

The HTTP method (GET, POST, PUT, DELETE) already describes the action. Your URL should describe the resource.

BadGoodWhy
GET /getUsersGET /usersGET already means "get"
POST /createUserPOST /usersPOST already means "create"
PUT /updateUser/1PUT /users/1PUT already means "update"
DELETE /deleteUser/1DELETE /users/1DELETE already means "delete"
// Express.js example with proper resource naming
const router = express.Router();

router.get("/users", listUsers);           // List all users
router.get("/users/:id", getUser);         // Get one user
router.post("/users", createUser);         // Create user
router.put("/users/:id", updateUser);      // Replace user
router.patch("/users/:id", patchUser);     // Partial update
router.delete("/users/:id", deleteUser);   // Delete user

// Nested resources for relationships
router.get("/users/:id/posts", getUserPosts);   // User is posts
router.post("/users/:id/posts", createUserPost); // Create post for user

2. Use proper HTTP status codes

// Status codes you should actually use
200 OK              // Successful GET, PUT, PATCH
201 Created         // Successful POST (include Location header)
204 No Content      // Successful DELETE
400 Bad Request     // Validation error, malformed request
401 Unauthorized    // Missing or invalid authentication
403 Forbidden       // Authenticated but not authorized
404 Not Found       // Resource does not exist
409 Conflict        // Duplicate resource (e.g., email already exists)
422 Unprocessable   // Valid JSON but semantic errors
429 Too Many Req    // Rate limit exceeded
500 Internal Error  // Server bug (never expose details)

// Example implementation
app.post("/users", async (req, res) => {
  try {
    const { email, name } = req.body;

    if (!email || !name) {
      return res.status(400).json({
        error: "Bad Request",
        message: "Email and name are required"
      });
    }

    const existing = await User.findByEmail(email);
    if (existing) {
      return res.status(409).json({
        error: "Conflict",
        message: "A user with this email already exists"
      });
    }

    const user = await User.create({ email, name });
    return res.status(201).json(user);
  } catch (err) {
    return res.status(500).json({ error: "Internal Server Error" });
  }
});

3. Implement consistent error responses

// Always use the same error format
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      { "field": "email", "message": "Must be a valid email address" },
      { "field": "age", "message": "Must be a positive integer" }
    ]
  }
}

4. Pagination is not optional

// GET /users?page=2&limit=20
app.get("/users", async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = Math.min(parseInt(req.query.limit) || 20, 100);
  const offset = (page - 1) * limit;

  const [users, total] = await Promise.all([
    User.find().skip(offset).limit(limit),
    User.countDocuments()
  ]);

  res.json({
    data: users,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit),
      hasNext: page * limit < total,
      hasPrev: page > 1
    }
  });
});

5. Version your API from day one

// URL versioning (most common)
GET /api/v1/users
GET /api/v2/users

// In Express
app.use("/api/v1", v1Router);
app.use("/api/v2", v2Router);

6. Use filtering, sorting, and field selection

// Filtering
GET /products?category=electronics&price_min=100&price_max=500

// Sorting
GET /products?sort=-price,name    // - prefix = descending

// Field selection (sparse fieldsets)
GET /users?fields=id,name,email   // Only return these fields

// Combined
GET /products?category=electronics&sort=-price&fields=name,price&page=1&limit=10

7. Secure your API properly

// Always use HTTPS
// Use Bearer token authentication
// Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

// Rate limiting middleware
const rateLimit = require("express-rate-limit");
app.use("/api/", rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                  // 100 requests per window
  standardHeaders: true,
  message: { error: "Too many requests, try again later" }
}));

// Input validation (never trust client data)
const { body, validationResult } = require("express-validator");
app.post("/users",
  body("email").isEmail().normalizeEmail(),
  body("name").trim().isLength({ min: 2, max: 100 }),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // proceed with creation
  }
);

8-10: Documentation, HATEOAS, and monitoring

8. Document everything. Use OpenAPI/Swagger to auto-generate interactive documentation. An undocumented API is an unusable API.

9. Consider HATEOAS for discoverability. Include links to related resources in your responses so clients can navigate the API without hardcoding URLs.

10. Monitor and log. Track response times, error rates, and usage patterns. Tools like Datadog or open-source alternatives like Grafana help you spot issues before your users do.

Troubleshooting common API issues

Issue 1: CORS errors in the browser. Add proper CORS headers. In Express: app.use(cors({ origin: "https://yourfrontend.com" })). Never use origin: "*" in production with credentials.

Issue 2: Slow responses. Add database indexes for filtered/sorted fields. Implement pagination. Use select() to return only needed fields. Add caching headers for GET requests.

Issue 3: Breaking changes affecting clients. Always version your API. Deprecate old versions with warning headers before removing them. Give clients at least 6 months to migrate.

Additional resources

J
Written by
Jesús García

Apasionado por la tecnologia y las finanzas personales. Escribo sobre innovacion, inteligencia artificial, inversiones y estrategias para mejorar tu economia. Mi objetivo es hacer que temas complejos sean accesibles para todos.

Share post:

Related posts

Comments

Leave a comment

Recommended Tools

The ones we use in our projects

Affiliate links. No extra cost to you.

Need technology services?

We offer comprehensive web development, mobile apps, consulting, and more.

Web Development Mobile Apps Consulting