Skip to main content

The Interface-First Approach

In Contract, a service is defined by two parts:
  1. An interface - Declares what operations your API supports (the “what”)
  2. An implementation - Contains the actual business logic (the “how”)
This separation is intentional and powerful. The interface is your API contract - it’s the source of truth for what your service can do. The implementation is a detail that clients don’t need to know about. We recommend organizing each service in its own Go package. This lets you use clean, simple names like API and Service - the package name provides the context:

Why Interface-First?

Before diving into the details, let’s understand why this approach is valuable.

Compile-Time Safety

If your implementation doesn’t match the interface, Go’s compiler tells you immediately - before your code even runs:
The compiler catches mismatches before your code even runs. This is a huge advantage over dynamic approaches where you might only discover problems when users hit a bug in production.

Clear Contracts

The interface IS your API documentation. Anyone can read it and understand exactly what your service does, without digging through implementation details:

Easy Testing

Because the interface separates “what” from “how”, you can easily create mock implementations for testing:

Multiple Implementations

You can have different implementations for different situations - all sharing the same interface:

Package Organization

We recommend organizing each service in its own package with a consistent structure:
Why this structure?
  1. Package-based naming: todo.API is clearer than TodoAPI, and user.Service is clearer than userService
  2. Encapsulation: Each package is self-contained with its own types
  3. Discoverability: Finding the todo API? Look in the todo package
  4. Scalability: Adding a new service? Create a new package

Defining Your Interface

Basic Structure

Every interface method must follow specific patterns. The rules are straightforward:

Method Signature Patterns

Contract supports four method patterns. Let’s explore each one with detailed explanations.

Pattern 1: Input and Output

The most common pattern - receive data, return data:
Breakdown:
  • ctx context.Context - Required first parameter for request context
  • in *InputType - Pointer to a struct containing the input data
  • *OutputType - Pointer to a struct containing the result
  • error - Always the last return value; nil means success
Example:
When to use: Most CRUD operations, calculations, queries - any method that receives data and returns data.

Pattern 2: Output Only (No Input)

Return data without receiving any input parameters:
Breakdown:
  • No input parameter beyond context
  • Still returns output and error
Example:
When to use: Listing all items, getting current status, dashboard data, health checks that return information.

Pattern 3: Input Only (No Output)

Accept input but return only success/failure:
Breakdown:
  • Accepts input data
  • Only returns error (nil means success)
  • No meaningful data to return
Example:
When to use: Delete operations, fire-and-forget updates, operations where you only care about success/failure.

Pattern 4: No Input or Output

Just do something and report success/failure:
Breakdown:
  • Only context as input
  • Only error as output
  • Simplest possible signature
Example:
When to use: Health checks, ping endpoints, administrative operations with no configuration.

Defining Input and Output Types

Types define the shape of data that flows through your API. Good type design makes your API clear and easy to use.

Input Types

Input types describe what clients send to your methods. They should only contain fields that clients actually need to provide:
Key points:
  • Use pointer types in method signatures: *CreateInput
  • Use json tags to control field names in JSON
  • Add omitempty for optional fields (tells JSON encoder to skip empty values)
  • Don’t include server-generated fields (like ID, CreatedAt)

Output Types

Output types describe what you return to clients. They can include computed and server-generated fields:

Why Separate Input and Output Types?

You might wonder why we don’t just use one User type everywhere. Here’s why separation is valuable:

Nested Types

For complex data, nest types within each other. This creates clear, reusable structures:

Implementing Your Interface

Now let’s implement the interface with actual business logic.

Basic Implementation

Create a struct that implements all interface methods:

With Multiple Dependencies

Real services often need multiple dependencies. Inject them all through the constructor:

Compile-Time Interface Check

Always add a compile-time check to ensure your implementation is complete. This catches missing methods immediately:
If you forget to implement a method, you’ll see a clear error:

Method Naming Conventions

Method names affect how they’re exposed via REST. Contract uses naming conventions to determine HTTP methods: Examples:

Using Context

Every method receives context.Context as its first parameter. Context is Go’s standard way to pass request-scoped data and control request lifecycle.

Cancellation

Check if the client cancelled the request. This is important for long-running operations:

Timeouts

Pass context to database and external calls. The context carries timeout information:

Request-Scoped Values

Access values set by middleware (like authenticated user):

Complete Example

Here’s a complete, production-ready service demonstrating all the concepts:

Best Practices

Keep Services Focused

Each service should handle one domain. Don’t create “god services” that do everything:

Use Clear, Consistent Naming

Names should be descriptive and follow a consistent pattern:

Inject Dependencies

Pass dependencies through the constructor, never use globals:

Keep Business Logic in the Implementation

The interface should only declare operations. All logic goes in the implementation:

Common Questions

Can I have multiple return values?

No, methods can only return (*Output, error) or error. If you need to return multiple values, wrap them in a struct:

Should input types be pointers?

Yes, always use pointers in the method signature:
Contract requires pointers for proper JSON unmarshaling and to support nil inputs.

How do I handle optional fields?

Fields not provided by clients will have their zero value. For most cases, this works fine:
For fields where you need to distinguish “not sent” from “sent as empty/zero”, use pointers:

What’s Next?

Now that you know how to define services: