Skip to content

Part 8.3 — Designing REST APIs That Feel Like GraphQL

Site Console Site Console
3 min read Updated Feb 7, 2026 Backend Development 0 comments

Most teams reach for GraphQL because their REST APIs feel rigid.
But rigidity is not a REST requirement—it’s a design failure.

In this post, you’ll design REST APIs that give clients flexibility without:

  • dynamic query languages

  • runtime query planners

  • field-level auth complexity

You’ll use clear contracts, explicit parameters, and disciplined DTOs.


1. The Goal: Client Control Without Chaos

GraphQL gives clients control by letting them specify fields.

In REST, you achieve the same goal by:

  • defining safe degrees of freedom

  • keeping response shapes predictable

  • making flexibility explicit

The key is constraint.


2. Start with Narrow, Purpose-Built DTOs

Avoid this anti-pattern:

return prisma.user.findUnique({ where: { id } });

This leaks:

  • database fields

  • relations

  • internal structure

Instead, define DTOs intentionally:

export class UserSummaryDto {
  id: number;
  name: string;
}
export class UserDetailDto extends UserSummaryDto {
  email: string;
}

Different DTOs = different contracts.


3. Field Selection via Query Parameters

Allow clients to request subsets, not arbitrary fields.

Example:

GET /users/1?fields=summary
GET /users/1?fields=detail

In controller:

@Get(':id')
getUser(
  @Param('id') id: number,
  @Query('fields') fields: 'summary' | 'detail' = 'summary',
) {
  return this.usersService.findById(id, fields);
}

This is safer than free-form field lists and easier to cache.


4. Implementing Field Variants in Services

async findById(id: number, fields: 'summary' | 'detail') {
  if (fields === 'summary') {
    return this.prisma.user.findUnique({
      where: { id },
      select: { id: true, name: true },
    });
  }

  return this.prisma.user.findUnique({
    where: { id },
    select: { id: true, name: true, email: true },
  });
}

Benefits:

  • explicit contracts

  • predictable performance

  • no runtime query building


5. Includes for Nested Data (Explicit Only)

Avoid implicit nesting.

Bad:

GET /users/1
→ returns posts, comments, likes, tags…

Good:

GET /users/1?include=posts
GET /users/1?include=posts.comments

Controller:

@Get(':id')
getUser(
  @Param('id') id: number,
  @Query('include') include?: 'posts' | 'posts.comments',
) {
  return this.usersService.findById(id, include);
}

Explicit includes keep APIs understandable.


6. Prisma Makes This Safe and Fast

Prisma’s select and include map perfectly to this pattern.

include === 'posts'
  ? { posts: true }
  : include === 'posts.comments'
  ? { posts: { include: { comments: true } } }
  : undefined;

You stay in control of:

  • query shape

  • joins

  • performance

No N+1 surprises.


7. Pagination Is Not Optional

GraphQL encourages pagination everywhere.
REST should too.

GET /posts?page=1&limit=20

Service:

findMany(page: number, limit: number) {
  return this.prisma.post.findMany({
    skip: (page - 1) * limit,
    take: limit,
    orderBy: { createdAt: 'desc' },
  });
}

Never return unbounded lists.


8. Sorting and Filtering as First-Class Concepts

Instead of new endpoints:

GET /posts?sort=createdAt&order=desc
GET /posts?authorId=1

This mirrors GraphQL arguments—cleanly.

Validate inputs strictly.
Never pass raw query params to Prisma.


9. Versioning by Evolution, Not Explosion

GraphQL avoids versioning by evolving schemas.

REST can do the same by:

  • adding fields, not changing them

  • adding new DTO variants

  • keeping old contracts intact

Avoid /v2 unless absolutely necessary.


10. Caching Is Still a Superpower

REST keeps one major advantage over GraphQL:

HTTP caching.

Your APIs:

  • are cacheable

  • debuggable

  • observable

By keeping response shapes stable per query, you preserve this advantage.


11. Error Shapes Must Be Predictable

GraphQL always returns structured errors.

REST should too.

Define a standard error response and stick to it.
Never leak stack traces or raw DB errors.


12. The Design Discipline That Matters

GraphQL doesn’t magically fix APIs.

It forces discipline.

You can apply that same discipline by:

  • designing DTOs intentionally

  • limiting flexibility

  • being explicit about includes

  • respecting performance boundaries

Well-designed REST APIs feel boring—and that’s a compliment.


13. What Comes Next

Now that you know how to:

  • shape REST responses intentionally

  • avoid over-fetching

  • give clients controlled flexibility

Related

Leave a comment

Sign in to leave a comment.

Comments