The Interface-First Approach
In Contract, a service is defined by two parts:- An interface - Declares what operations your API supports (the “what”)
- An implementation - Contains the actual business logic (the “how”)
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: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:- Package-based naming:
todo.APIis clearer thanTodoAPI, anduser.Serviceis clearer thanuserService - Encapsulation: Each package is self-contained with its own types
- Discoverability: Finding the todo API? Look in the
todopackage - 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:ctx context.Context- Required first parameter for request contextin *InputType- Pointer to a struct containing the input data*OutputType- Pointer to a struct containing the resulterror- Always the last return value; nil means success
Pattern 2: Output Only (No Input)
Return data without receiving any input parameters:- No input parameter beyond context
- Still returns output and error
Pattern 3: Input Only (No Output)
Accept input but return only success/failure:- Accepts input data
- Only returns error (nil means success)
- No meaningful data to return
Pattern 4: No Input or Output
Just do something and report success/failure:- Only context as input
- Only error as output
- Simplest possible signature
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:- Use pointer types in method signatures:
*CreateInput - Use
jsontags to control field names in JSON - Add
omitemptyfor 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 oneUser 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:Method Naming Conventions
Method names affect how they’re exposed via REST. Contract uses naming conventions to determine HTTP methods:| Method Name Pattern | REST Endpoint | HTTP Method |
|---|---|---|
Create, Add, New | /{resource} | POST |
List, All, Search | /{resource} | GET |
Get, Find, Fetch | /{resource}/{id} | GET |
Update, Edit, Set | /{resource}/{id} | PUT |
Delete, Remove | /{resource}/{id} | DELETE |
Patch | /{resource}/{id} | PATCH |
| Other names | /{resource}/{action} | POST |
Using Context
Every method receivescontext.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:How do I handle optional fields?
Fields not provided by clients will have their zero value. For most cases, this works fine:What’s Next?
Now that you know how to define services:- Registration - How to register your service with Contract
- Error Handling - Handle errors consistently across protocols
- Type System - Deep dive into type conversion and schemas
- Testing - Test your services effectively