# The REST API Maturity Curve: From Junior to Tech Lead

“How your approach to API design evolves as your engineering responsibilities grow”

![](https://www.pexels.com/photo/programmer-holding-a-paper-cutout-with-an-api-quote-11035364/ align="center")

![](https://cdn-images-1.medium.com/max/1080/1*PIG34oqcae3nHax24_Xjhw.jpeg align="left")

Photo by RealToughCandy.com: [https://www.pexels.com/photo/programmer-holding-a-paper-cutout-with-an-api-quote-11035364/](https://www.pexels.com/photo/programmer-holding-a-paper-cutout-with-an-api-quote-11035364/)

**“The integration worked in staging, but blew up in production.”**

**“The client relied on a field we never intended to expose.”**

**“We added a small change and broke three downstream systems.”**

The above cases can be quite common when you’re primary work involves a lot of API development. In real-world engineering, *how* you understand API requirements is just as important as *what* the requirements say.

I intend to break down the evolution of API understanding through three lenses — **Junior**, **Senior**, and **Tech Lead** — based on how I’ve seen and experienced these roles.

### Read this before going further

* **Titles don’t have a real value unless the associated responsibilities are truly realized**. The post is not intended to talk about individual’s performance.
    
* **Code snippets written here are not tested**. They are just there to explain a point visually.
    
* I’m not explaining the most common problems at each level and fixes. **The main idea is to share my interpretation of these roles.**
    

### What Do “API Requirements” Actually Mean?

For many, API requirements = endpoint + method + payload + response. That’s the bare minimum. In practice, it includes:

* **Functional constraints**: What the API must do.
    
* **Contextual expectations**: Why it exists, who uses it, where it fits in the system.
    
* **Systemic qualities**: Performance, rate limits, error semantics, observability.
    
* **Temporality**: What breaks if we change it? Is it versioned? Is it backward-compatible?
    

API endpoints can be assumed like contracts for communication.

### Junior Engineer: Fulfilling the Contract

At this stage, you’re mostly writing code as instructed or requested — follow the provided Jira ticket or relevant extract from a big spec document.

**Typical focus areas:**

* HTTP verb and route structure
    
* Payload fields and response types
    
* Return correct status codes
    
* Implement/update the unit tests
    

**Where things go wrong:**

* Incomplete requirements from your source
    
* Misunderstand optional vs. required fields
    
* Don’t consider how the API is used downstream (e.g. other systems, third-party clients)
    
* Assume “happy path” is sufficient
    

**How I tried to improve**

A simple question and a perspective shift helped me improve my solution capability to implement a better solution.

> What assumptions were made for this task? (or spec?) *and* reading the API spec like a consumer, not just a coder

### Senior Engineer: Seeing Beyond the Contract

Now you’re thinking about how this API behaves *in production*, not just if it *works.*

**Your questions evolve:**

* How do we handle edge cases (e.g. deleted users, archived states)?
    
* Is this exposing sensitive or inconsistent data?
    
* How does this interact with pagination, search, localization?
    
* Should we debounce calls or cache on client/server?
    

**A problem scenario:**  
Here’s something that illustrates a common real-world problem with leaking internal fields in an API response. You once exposed a `GET /users/:id` that returned full user objects.

```typescript
// user.model.ts (Prisma ORM model or internal DB schema)
export const user = await prisma.user.findUnique({ where: { id } });
res.json(user); // Assume this exposes everything, including sensitive/internal fields like isAdmin, lastLoginIp.
```

Fields like `isAdmin`, `lastLoginIp` are unintentionally exposed. If another team starts depending on them, changing those fields later becomes a breaking change.

A common fix to it is using DTOs and field-level control.

```typescript
// dto/PublicUserDTO.ts
import { Exclude, Expose } from "class-transformer";

export class PublicUserDTO {
  @Expose()
  id: string;
  @Expose()
  name: string;
  @Expose()
  avatarUrl: string;
  @Expose()
  joinedAt: Date;
  @Exclude()
  isAdmin: boolean;
  @Exclude()
  lastLoginIp: string;
  constructor(partial: Partial<PublicUserDTO>) {
    Object.assign(this, partial);
  }
}

// ---------------------------------------------------------
// user.controller.ts
import { plainToInstance } from "class-transformer";

app.get("/users/:id", async (req, res) => {
  const user = await prisma.user.findUnique({ where: { id: req.params.id } });
  if (!user) return res.status(404).json({ message: "User not found" });
  const publicUser = plainToInstance(PublicUserDTO, user, {
    excludeExtraneousValues: true,
  });
  res.json(publicUser); // Only exposes whitelisted fields
});
```

* This keeps your **public API contract stable**, even as your internal models evolve.
    
* Adding/removing fields becomes a **controlled change**, not a surprise.
    

> Never forget that if it’s in the API, someone will depend on it. Consider minimal responses from the early stages. Expand as required after careful data-driven decisions

### Tech Lead: Designing for Time, Teams, and Trade-offs

At this level, you’re no longer just shipping endpoints; you’re shaping **how systems communicate, evolve, and scale across teams**.

**Your focus shifts:**

* Will adding a required query param break downstream consumers?
    
* Does this align with the REST patterns used across other teams?
    
* What happens if a dependent service goes down mid-request?
    
* Can we detect and respond to API degradation before users feel it?
    
* Are we truly seeing the consumer perspective?
    

Often, the most important API requirements are the ones **not written down**:

* What are clients assuming implicitly?
    
* Is this endpoint used synchronously or in batch mode?
    
* Is this API part of a contract with an external partner?
    
* Will this be consumed by mobile apps (with bad network)?
    

You learn to *ask better questions*:

* “Why does this need to be real-time?”
    
* “What happens if the field is missing?”
    
* “Who else might be affected if we change this?”
    
* “How frequently will the user call this?”
    

You’re no longer just writing code-you’re **designing long-term contracts**. And often, the hardest work is **navigating ambiguity**, aligning cross-team needs, and making intentional trade-offs.

**A problem scenario:**  
You’re building a **User Profile API** used by internal HR tools, customer-facing dashboards, and reporting systems. Each use case needs different slices of data, and some rely on slow or failure-prone downstream services.

> *Do you build one endpoint to serve all? Or a clean core endpoint with sidecar APIs?*

Initially, you build a single endpoint to serve all needs-but it quickly becomes fragile and hard to evolve.

```typescript
// One Endpoint Trying to Serve All Use Cases
app.get("/api/v3/users/:userId", async (req, res) => {
  const user = await profileService.fetch(userId);
const location = await geoService.lookup(user.locationId); // Assuming it's a 3rd party service, it might fail or be slow
  const adminFlags = await hrService.getAdminFlags(user.id); // Not needed by all consumers
  res.json({
    id: user.id,
    name: `${user.firstName} ${user.lastName}`,
    email: user.email,
    location,
    adminFlags,
  });
});
```

The above code is fragile. A single service failure takes down the entire response. Everyone gets all the data, whether they need it or not.

```typescript
// DTO + Fallbacks is one way to solve this problem
// dto/PublicUserProfileDTO.ts
import { Expose } from "class-transformer";
export class PublicUserProfileDTO {
  @Expose() id: string;
  @Expose() name: string;
  @Expose() avatarUrl: string;
  @Expose() location?: string;
  static from(user: any, location?: string): PublicUserProfileDTO {
    return {
      id: user.id,
      name: `${user.firstName} ${user.lastName}`,
      avatarUrl: user.avatarUrl,
      location,
    };
  }
}
// ---------------------------------------------------------
// user.controller.ts
import { plainToInstance } from "class-transformer";
import { PublicUserProfileDTO } from "../dto/PublicUserProfileDTO";
app.get("/api/v3/users/:userId", async (req, res) => {
  onst user = await profileService.fetch(req.params.userId);
  const role = req.user?.role || "public"; // Confirm the role
  
  if (role === "admin") {
    adminFlags = await hrService.getAdminFlags(user.id);
  }
  let location: string | undefined;
  try {
    location = await geoService.lookup(user.locationId);
  } catch {
    location = undefined; // Fallback. dummy example. Please don't come after me saying it's against Typescript philosophy
  }
  const response = plainToInstance(
    PublicUserProfileDTO,
    PublicUserProfileDTO.from(user, location),
    { excludeExtraneousValues: adminFlags } // Get only required content
  );
  res.json(response);
});
```

* Only explicitly exposed fields are sent; internal schema changes won’t leak into the response.
    
* Having a fallback ensures predictable responses if other services fail.
    
* You can create new DTOs for future API versions without touching core logic.
    

You’re no longer optimizing just for **functionality**, but for **resilience, collaboration, and change over time**.

Because at this level, the question isn’t just *“Does the API work?”*; it’s *“Will it still work X months/years from now, under pressure, across clients?”*

### Takeaways

* **Junior** engineers think about correctness of their implementation.
    
* **Seniors** think about robustness and user experience.
    
* **Leaders** think about longevity and ecosystem alignment.
    
* True API skill isn’t writing endpoints — it’s *understanding requirements in multiple dimensions*.
    

> *Good engineers implement APIs. Great engineers challenge the need for them.*

---

What API lesson left an impact on you at job? Or if you’re earlier in your journey: What part of API design still feels unclear?

Drop a comment or share your favorite war story. Let’s grow together.

Until next time 👋
