Skip to main content

Expressive Code at Scale: How Snitch Transforms API Development

· 9 min read
Snitch Team
Snitch Framework Maintainers

In the world of software development, there's a persistent myth that we must choose between code that is pleasant to write and code that scales well technically. Many believe that as systems grow, elegance must give way to verbosity, and expressivity must be sacrificed for safety and performance.

Snitch challenges this false dichotomy by combining the best of both worlds: an incredibly expressive DSL with the rigorous type safety of Kotlin. Let's explore why expressivity matters at scale, and how Snitch pushes the boundaries of what's possible.

The Hidden Costs of Unexpressive Code

When we talk about "scaling" software, we usually focus on technical metrics: requests per second, response times, resource utilization. But there's another crucial scaling dimension that gets less attention: human scaling.

Human scaling refers to how well your codebase can grow while remaining comprehensible and maintainable by a team of developers.

Unexpressive code creates an enormous tax on human scaling:

  1. Cognitive load: Developers must mentally translate between what the code says and what it means
  2. Onboarding friction: New team members take longer to become productive
  3. Maintenance burdens: Bug fixes and feature additions require more careful analysis
  4. Knowledge silos: Implementation details become specialized knowledge
  5. Refactoring hesitancy: Complex code discourages necessary evolution

These costs compound as systems grow larger, often leading to what's colloquially known as "technical debt" - a debt that's paid through slower development cycles, more bugs, and ultimately, competitive disadvantage.

Expressivity's Evolution: From Ruby on Rails to Kotlin DSLs

The software industry has long recognized the value of expressive code. Ruby on Rails, which emerged in 2004, built much of its success on prioritizing "developer happiness" and the principle of convention over configuration.

Ruby on Rails: The Expressivity Pioneer

Ruby on Rails showed that a framework could be both powerful and a joy to use:

class UsersController < ApplicationController
before_action :require_login

def index
@users = User.all
end

def show
@user = User.find(params[:id])
end
end

This code is remarkably readable - almost English-like in its clarity. But as Rails applications grew, they often encountered challenges:

  1. Runtime errors: The lack of compile-time type checking meant many errors weren't caught until execution
  2. Performance challenges: The very features that made Rails expressive often came with performance costs
  3. Magic: Rails' "convention over configuration" sometimes felt like "magic" that was hard to debug
  4. Testing burden: Without type checks, comprehensive test coverage became absolutely essential

The Rise of Typed Functional Languages

The pendulum then swung in the other direction. Languages like Scala, Haskell, and later TypeScript gained popularity partly as a reaction to the perceived "unsafety" of dynamic languages like Ruby.

Consider this Scala example using the Play Framework:

def getUser(id: Long) = Action { 
userRepository.findById(id) match {
case Some(user) => Ok(Json.toJson(user))
case None => NotFound("User not found")
}
}

This code benefits from strong typing and pattern matching, but loses some of the immediate readability that made Rails so appealing.

The Best of Both Worlds: Kotlin DSLs

Kotlin emerged as a language that could provide the safety of static typing with the expressiveness of a modern language. Its support for DSLs (Domain Specific Languages) opened up new possibilities for frameworks.

Here's a simple example using Kotlin's built-in HTML DSL:

html {
head {
title("My Page")
}
body {
h1 { +"Welcome" }
p { +"This is a paragraph" }
}
}

This code is both expressive and type-safe. Errors like misspelling a tag name or using the wrong nesting structure would be caught at compile time.

Snitch: Pushing the Boundaries of Expressive, Type-Safe Code

Snitch takes the concept of expressive, type-safe DSLs to a new level for HTTP APIs. Let's compare a typical REST endpoint in some popular frameworks to see the evolution:

Spring Boot (Java)

@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository;

@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userRepository.findById(id)
.map(user -> ResponseEntity.ok(user))
.orElse(ResponseEntity.notFound().build());
}

@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<User> createUser(@Valid @RequestBody UserDTO userDTO) {
User user = new User(userDTO.getName(), userDTO.getEmail());
User savedUser = userRepository.save(user);
return ResponseEntity.created(URI.create("/api/users/" + savedUser.getId()))
.body(savedUser);
}
}

Ktor (Kotlin)

routing {
route("/api/users") {
get("/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID format")
return@get
}

val user = userRepository.findById(id)
if (user != null) {
call.respond(user)
} else {
call.respond(HttpStatusCode.NotFound, "User not found")
}
}

authenticate {
post {
val userDTO = call.receive<UserDTO>()
if (!isAdmin(call.principal)) {
call.respond(HttpStatusCode.Forbidden)
return@post
}

val user = User(userDTO.name, userDTO.email)
val savedUser = userRepository.save(user)
call.respond(HttpStatusCode.Created, savedUser)
}
}
}
}

Snitch (Kotlin)

val userId by path(ofLong)
val userDTO by body<UserDTO>()

val usersRouter = routes {
"api" / "users" / {
GET(userId) isHandledBy {
userRepository.findById(request[userId])
?.ok
?: "User not found".notFound()
}

POST() with userDTO onlyIf hasAdminRole isHandledBy {
val user = User(request[userDTO].name, request[userDTO].email)
val savedUser = userRepository.save(user)
savedUser.created
}
}
}

Snitch's approach offers several distinctive advantages for expressivity while maintaining type safety:

  1. Declarative path definition: Routes are defined hierarchically and read naturally from left to right
  2. Parameter declaration separation: Parameters are defined once and reused, reducing repetition
  3. Infix notation: isHandledBy, with, and onlyIf create an almost English-like readability
  4. Result DSL: .ok, .notFound(), and .created make response codes clear and concise
  5. Type safety throughout: From parameter parsing to handler execution, everything is type-checked
  6. No annotations: Logic flows naturally without being broken up by annotations
  7. Composability: Routes can be modularized and composed easily

Expressivity and Scaling: An Unexpected Synergy

Here's where Snitch truly shines - as your API grows more complex, Snitch's expressivity becomes even more valuable, not less.

Consider a more complex API with nested resources, multiple role-based permissions, and sophisticated validation:

val userId by path(ofLong)
val postId by path(ofLong)
val commentId by path(ofLong)
val limit by query(ofIntRange(1, 100), default = 20)
val offset by query(ofNonNegativeInt, default = 0)
val postBody by body<CreatePostRequest>()
val commentBody by body<CreateCommentRequest>()

val apiRoutes = routes {
"api" / "v1" / {
"users" / userId / {
GET() onlyIf (isResourceOwner(userId) or hasAdminRole) isHandledBy {
getUserProfile()
}

"posts" / {
GET() withQueries(limit, offset) isHandledBy {
getUserPosts()
}

POST() with postBody onlyIf isResourceOwner(userId) isHandledBy {
createPost()
}

postId / {
GET() isHandledBy {
getPost()
}

PUT() with postBody onlyIf (
isResourceOwner(userId) and isPostOwner(postId)
) isHandledBy {
updatePost()
}

"comments" / {
GET() withQueries(limit, offset) isHandledBy {
getPostComments()
}

POST() with commentBody onlyIf isAuthenticated isHandledBy {
createComment()
}

commentId / {
DELETE() onlyIf (
isCommentAuthor(commentId) or
isPostOwner(postId) or
hasModeratorRole
) isHandledBy {
deleteComment()
}
}
}
}
}
}
}
}

Despite the complexity of this API, the code remains remarkably readable. You can trace the URL structure visually through the nesting, see the HTTP methods at a glance, and understand the authorization rules directly in the route definitions.

This is where the true value proposition of Snitch emerges: as complexity increases, the code doesn't degrade into an unreadable mess - it maintains its clarity and expressiveness.

The Unfair Advantage: Type Safety + Expressivity

Most frameworks force you to choose between expressivity and type safety. Snitch gives you both, creating what we might call an "unfair advantage" in three key areas:

1. Error Prevention

Traditional expressive frameworks like Rails and Express catch errors at runtime. Snitch catches them at compile time:

// This would not compile - userId is not defined
GET(userId) isHandledBy { getUserProfile() }

// This would not compile - wrong parameter type
val userId by path(ofString)
userRepository.findById(request[userId].toLong()) // Type mismatch

// This would not compile - missing required parameter
POST() isHandledBy { createUser() } // Body parameter missing

2. Refactoring Confidence

When you need to refactor a large API, type safety is invaluable:

// Rename a parameter
val oldUserId by path(ofLong) // Deprecated
val userId by path(ofLong) // New name

// The compiler will flag every place oldUserId is used

3. Self-Documenting Code

Snitch's expressive DSL makes the code itself excellent documentation:

// The route structure visually represents the URL hierarchy
// HTTP methods are capitalized and stand out
// Parameters are clearly defined
// Authorization rules are spelled out in the route definition

And because Snitch automatically generates OpenAPI documentation from this code, your API docs are always in sync with the implementation.

Industry Impact: Changing How We Think About API Development

The combination of expressivity and type safety in Snitch has potential implications for the entire field of API development:

  1. Reduced testing burden: With many errors caught at compile time, tests can focus on business logic rather than basic type checking
  2. Improved developer experience: Frameworks that prioritize both safety and expressivity will set a new standard for developer experience
  3. More maintainable codebases: As projects age, the value of self-documenting, type-safe code increases dramatically
  4. Faster onboarding: New team members can understand the API structure more quickly and make contributions with greater confidence
  5. Higher quality APIs: By making good practices the easy path, frameworks like Snitch nudge developers toward better API design

Conclusion: Expressivity as a Competitive Advantage

In the early days of web development, expressivity was often seen as a nice-to-have feature - something that made developers happy but didn't necessarily contribute to business outcomes. Today, we understand that expressive, maintainable code is a genuine competitive advantage:

  • It allows teams to respond more quickly to changing requirements
  • It reduces bugs and security vulnerabilities
  • It makes it easier to attract and retain talented developers
  • It lowers the long-term cost of maintenance

Snitch demonstrates that expressivity and type safety are not opposing forces but complementary strengths. By bringing them together in a cohesive framework, Snitch offers a glimpse of the future of API development - one where we no longer have to choose between code that is a joy to write and code that scales robustly.

As the complexity of our systems continues to grow, frameworks that help us manage that complexity without sacrificing readability will become increasingly valuable. Snitch isn't just a more pleasant way to build APIs - it's a more sustainable approach to software development in an increasingly complex world.