Skip to content
Midnight
Back to blog
Engineering

API Design Patterns for Modern Developer Tools

How we designed Midnight's API to be developer-friendly, consistent, and scalable — with practical patterns you can apply to your own products.

SK
Sarah Kim

APIs are the backbone of modern developer tools. A well-designed API can make your product indispensable; a poorly designed one creates friction that compounds with every integration. After building and iterating on Midnight’s API for two years, here are the patterns that have served us best.

Start With Use Cases, Not Resources

The instinct when designing an API is to model your database tables as REST resources. Resist that urge. Start with the workflows developers need to accomplish.

For Midnight, we identified five primary use cases before writing a single endpoint:

  • Create and manage projects programmatically
  • Query tasks with complex filters (assignee, status, date range, custom fields)
  • Subscribe to events for real-time integrations
  • Bulk operations for migrations and batch updates
  • Read analytics data for custom dashboards

These use cases drove our resource design, not the other way around. The result is an API that feels intuitive because it maps to real workflows rather than internal data structures.

Consistency Is More Important Than Cleverness

Every endpoint should feel like it belongs to the same API. We enforce consistency through a strict set of conventions:

  • Naming: All resources use plural nouns (/projects, /tasks, /webhooks). No verbs in URLs.
  • Pagination: Every list endpoint uses cursor-based pagination with limit and cursor parameters. Never offset-based.
  • Filtering: All list endpoints support the same filter syntax: ?status=active&assignee=user_123.
  • Responses: Every response wraps data in a { data, meta } envelope. Errors use { error: { code, message, details } }.

These conventions mean developers who learn one endpoint can predict how every other endpoint works. The cognitive load drops dramatically.

Rate Limiting That Developers Don’t Hate

Rate limiting is necessary, but the implementation matters enormously for developer experience. We learned three things:

  1. Be generous with limits. Our default is 1,000 requests per minute. Most developers never hit it, but the generous ceiling means they don’t have to worry about it during development.

  2. Return remaining quota in headers. Every response includes X-RateLimit-Remaining and X-RateLimit-Reset. Developers can build smart retry logic without guessing.

  3. Use sliding windows, not fixed windows. Fixed windows create thundering herd problems at the boundary. A sliding window smooths out the distribution.

Webhooks Done Right

Webhooks are how integrations stay in sync. Most webhook implementations are fragile. Ours follows these principles:

  • Signed payloads. Every webhook includes an HMAC signature so receivers can verify authenticity.
  • Retry with exponential backoff. Failed deliveries retry 5 times over 24 hours. If all retries fail, we disable the webhook and notify the owner.
  • Event types are granular. Instead of one task.updated event with a diff, we send task.status_changed, task.assigned, task.due_date_changed. This lets consumers subscribe to exactly what they need.
  • Idempotency keys. Every event includes a unique event_id. Consumers can deduplicate in case of double delivery.

Versioning Strategy

API versioning is contentious. We chose URL-based versioning (/v1/, /v2/) for its simplicity and explicitness. Header-based versioning is clever but causes confusion in practice.

Our versioning rules:

  • Additive changes don’t bump the version. New fields, new endpoints, new query parameters — these are non-breaking and ship without a version bump.
  • Breaking changes get a new version. Removing fields, changing types, altering behavior — these require a new version with a migration guide.
  • Old versions are supported for 12 months. Developers get a full year to migrate. We send deprecation notices at 9 months and 11 months.

Error Messages That Help

The biggest API frustration is unhelpful error messages. “Invalid request” tells the developer nothing. Our error responses include:

  • A human-readable message explaining what went wrong
  • A machine-readable error code for programmatic handling
  • A details array with field-level validation errors
  • A docs_url pointing to the relevant documentation page

This investment in error quality reduces support tickets and makes the API genuinely pleasant to work with. The best API is one where developers rarely need to read the docs because the error messages guide them.


Good API design is not about following REST dogma or using the latest specification. It’s about empathy for the developers who will build on your platform. Every decision should reduce their cognitive load and help them ship faster.