How to Design APIs Like a Senior Engineer
backend
As developers, we’re often taught to build REST APIs following a standard set of conventions. We create multiple endpoints, use various HTTP verbs, and return a range of status codes like 200, 404, or 500. But what if this approach has fundamental flaws, especially for private, internal APIs?
A senior engineer’s perspective often challenges these conventions in favor of a more robust, predictable, and proprietary architecture. The goal is to build APIs that are not just functional, but also highly scalable, secure, and efficient. Let’s explore how to achieve this.
Core Architecture: A New Paradigm
The foundation of a senior-level API design moves away from the classic REST model and embraces a more consolidated and efficient structure.
1. Move Away from REST for Private APIs
Standard REST status codes can be misleading. A 500 error might hide a predictable validation failure, and a 404 doesn’t distinguish between a mistyped URL and a resource that genuinely doesn’t exist. For private APIs (those consumed by your own front end), we can do better. A proprietary design gives us more control and clarity.
2. Embrace a Single Endpoint and Batching
Instead of a sprawling collection of URLs (/users, /posts, /comments), consider using a single /api endpoint that only accepts POST requests. This might sound counterintuitive, but it unlocks a powerful capability: request batching.
Client applications can collect multiple actions (e.g., fetching user data, posting a comment, updating a setting) and send them to the server in a single network call. By batching requests at short intervals (e.g., every 50ms), you dramatically reduce network overhead and improve perceived performance.
3. A Clean, Folder-Based Structure
To keep the codebase organized, each API should live in its own folder. This self-contained module should include at least two key files:
handler.ts: The file containing the core execution logic for the API.constants.ts: A metadata file that defines the API’s behavior, such as its rate-limiting configuration and, most importantly, its validation schemas.
The Request Flow: Security and Predictability First
A senior engineer knows that an API’s primary responsibility is to be secure and predictable. This is achieved by establishing a strict, ordered flow for every incoming request.
1. Validation: The Most Critical Step
Before your handler’s logic ever runs, you must strictly validate the request body. This is non-negotiable. Tools like Zod are perfect for this, allowing you to define a precise schema for the expected input.
// src/content/blog/api/some-api/constants.ts
import { z } from 'zod';
export const SomeApiInputSchema = z.object({
userId: z.string().uuid(),
content: z.string().min(1).max(280),
});
Strict validation is your first line of defense against a host of vulnerabilities, including NoSQL injections. If the incoming data doesn’t match the schema, the request is rejected immediately. No exceptions.
2. Smart Rate Limiting
Rate limiting prevents abuse and ensures stability. The strategy should differ based on who is making the request:
- Unauthorized Users: Rate limit based on their IP address.
- Authorized Users: Rate limit based on their Account ID and the specific API they are calling. This is more granular and fair.
Pro-tip: You can embed the AccountID in a JWT-signed token. This allows your rate-limiting middleware to extract the ID without needing a costly database lookup, making the process incredibly fast.
3. Authorization: The First Line of Code
Security shouldn’t be an afterthought; it should be the very first thing you check. Every API handler should begin with an explicit authorization check.
// src/content/blog/api/some-api/handler.ts
import { requireAdmin } from '~/lib/auth';
export async function handler(input: SomeApiInput) {
requireAdmin(input.currentUser); // Throws an error if not an admin
// ... rest of the logic
}
This requireAdmin or requireUser function acts as a gatekeeper, ensuring that the rest of your code only executes if the user has the necessary permissions.
End-to-End Type Safety: The Holy Grail
While input schemas provide security, output schemas provide type safety and a superior developer experience. By defining a Zod schema for the data your API returns, you create a contract.
This contract can then be shared with your front-end application. Using a type generator, you can create a fully typed, end-to-end system. When the back end changes an output shape, the front-end code will immediately show a TypeScript error, catching bugs at compile time, not in production.
Trade-Offs and Considerations
This opinionated design is powerful, but it’s not a silver bullet. It’s crucial to understand the trade-offs.
Pros
- Improved Performance: Request batching dramatically reduces network latency for chatty applications.
- Enhanced Developer Experience: End-to-end type safety catches bugs early and makes development a breeze.
- Superior Security: A validation-first and authorization-first approach hardens your API from the ground up.
Cons
- Breaks REST Conventions: This is the biggest drawback. You lose standard HTTP caching, semantic verb meanings (
GET,PUT), and browser-friendly testing. - Poor for Public APIs: This pattern is ill-suited for third-party developers who expect standard RESTful interfaces.
- Tooling Complexity: Standard API tools like Swagger or Postman are built for REST and may not integrate seamlessly.
This architecture, similar to what libraries like tRPC popularize, is an excellent choice for modern, tightly-coupled applications where you control both the client and server. For public-facing APIs, sticking to REST is often the better choice.
Conclusion
Designing APIs like a senior engineer means prioritizing predictability, security, and performance. By moving away from traditional REST for private services and embracing concepts like single endpoints, request batching, strict validation, and end-to-end type safety, you can build a back-end architecture that is not only robust and scalable but also a joy to work with.
Latest Posts
A Practical Guide to Backing Up Your Linux Server: PostgreSQL, Jenkins, and More
A comprehensive guide for developers on how to back up critical services like PostgreSQL and Jenkins on a Linux server, and how to automate the process.
Mastering Real-Time Communication with Socket.IO Rooms
A deep dive into creating and managing public and private rooms in Node.js with Socket.IO for scalable, real-time applications.
Hoppscotch: The Modern, Lightweight Alternative to Postman and Bruno
A deep dive into Hoppscotch, the open-source API client, and why it might be the perfect replacement for Postman and Bruno in your workflow.
Enjoyed this article? Follow me on X for more content and updates!
Follow @Ctrixdev