Efficient Software Development Workflow: One Schema, Two Ends
Generate Server & Client API code from OpenAPI Specifications

I’m Jahswill Essien — a senior mobile engineer, technical writer, and product builder with over 8 years of experience building mobile applications, and 6+ years deeply invested in Flutter.
I don’t just build apps — I build products. From idea to execution, I focus on solving real problems, making intentional technical decisions, and shipping software that scales, performs, and delivers value. My background spans Android (Java/Kotlin), Flutter (Dart), Python for scripting, and an expanding interest in systems and backend technologies like Go and Rust.
Beyond shipping production-grade applications, I’m passionate about knowledge sharing and technical storytelling. I write to teach, simplify, and demystify complex concepts — often using visual explanations, diagrams, and analogies to make ideas stick. This approach has shaped my work as a technical author for platforms like LogRocket and Codemagic, and within the broader Flutter community.
I’m intentional about growth — in skills, in thinking, and in life. I believe great engineers are shaped not just by the code they write, but by how deeply they understand problems, communicate ideas, and collaborate to build meaningful products. If you’re interested in Flutter internals, mobile architecture, performance, developer tooling, or product-focused engineering, you’re in the right place.
You can explore my github page 👉 https://github.com/JasperEssien2
“We are constrained by our minds. What we feel is our limit isn’t our limit. It’s amazing how much we can learn in a short amount of time by perseverance alone.”
TL;DR
Stop manually writing API server or client code. Define your API schema first with OpenAPI, then auto-generate type-safe server stubs (Go) and client API code (Dart/Flutter). This workflow eliminates boilerplate, prevents integration bugs, and ensures your frontend and backend are always in sync.
Building software requires fast iteration, but doing so with efficiency is what sustains it. The traditional back-and-forth between backend and frontend teams, often caused by misunderstood APIs or integration issues, kills momentum. The solution? A single source of truth – an OpenAPI specification file. The team defines an agreed-upon schema before writing any single line of code. With this schema, you can auto-generate both server stubs and client-side code, thereby eliminating boilerplate and ensuring absolute consistency between your API and its consumers.
In this guide, you'll learn how to fast-track development using code generation. For demonstration, we'll use Go for server-side code generation and for client-side, Dart (for Flutter). Same OpenAPI specification different stacks.
What is OpenAPI specifications?
The OpenAPI Specification (formerly Swagger) is a standardized format for defining RESTful APIs. It is a language-agnostic declarative description of your API's endpoints, request/response structures, and authentication methods, written in readable YAML or JSON.
Because the spec is structured and declarative, tools can parse it to generate documentation, mock servers, and—most importantly—production-ready code.
A YAML file (with extension .yaml or .yml) is a lightweight data serialization language.
Defining OpenAPI schema
Efficiency means utilizing the right tools. Instead of writing the OpenAPI YAML file from scratch, you can prompt an AI assistant to do the heavy lifting. I'll use the prompt below:
"Generate an OpenAPI YAML schema for a user authentication service. Include endpoints for login, signup, get profile, and update profile. Define all necessary request bodies and response schemas."
Depending on your project's specifications, your prompt might be different. You could also decide to write this out manually (which is an inefficient use of time and resources, in my opinion). Whether generated by AI or written by hand, your schema will serve as the contract. Let's look at the structure of a basic OpenAPI document.
Basic syntax of a YAML file
- Key-value pairs: Colons followed by a space are use for key-value pairs:
key: value - Indentation: Spaces (not tabs) denote nesting and hierarchy
- List: Prefix items with
-to specify a list item.
The Anatomy of an OpenAPI File
openapi: The version of the OpenAPI spec (e.g., 3.1.0). info: Metadata like API title, description, and version. servers: A list of base URLs for your API environments. paths: Your API endpoints and their supported HTTP operations. components: Reusable building blocks (schemas, responses) to avoid repetition. security: Global or operation-level authentication requirements (e.g., JWT, OAuth).
openapi: 3.1.0
info:
title: Product API
version: 1.0.0
description: API for user authentication and profile management
servers:
- url: https://api.example.com/v1
paths:
# Define paths here
components:
# Define components here
security:
# define security here
Understanding Paths
The paths section defines your endpoints. Here is an example for a signup route:
paths:
/auth/signup:
post:
summary: Create a new user account
description: |
Registers a new user in the system.
This endpoint validates the provided email and password, creates the user record, and sends a verification email. It returns an authentication token upon success.
tags:
- Auth
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
- password
- name
properties:
name:
type: string
description: User's full name
email:
type: string
format: email
description: User's email address
password:
type: string
format: password
minLength: 8
description: User's password
responses:
'201':
description: User created successfully
content:
application/json:
schema:
$ref: "#/components/schemas/AuthResponse"
'400':
$ref: "#/components/responses/BadRequest"
This definition tells us exactly what the endpoint expects and what it returns. The $ref syntax points to a reusable object in the components section, keeping your spec DRY (Don't Repeat Yourself).
For a complete reference to structuring an OpenAPI spec, you can always check the official OpenAPI documentation.
Understanding component schema
The components section is used to declare reusable definitions for schemas, parameters, and security schemes. By defining these objects here and referencing them via $ref, you ensure consistency across your API and simplify maintenance
components:
schemas:
User:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
AuthResponse:
type: object
properties:
token:
type: string
user:
$ref: '#/components/schemas/User'
Security Definitions
We also define our security mechanisms here. For a JWT-based authentication, we define a bearerAuth scheme:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
Then, we can apply this security scheme globally (to all endpoints) or to specific endpoints:
security:
- bearerAuth: []
To learn more visit here
Generate server code in Go lang
Now that we have a good understanding of OpenAPI specification, using the oapi-codegen tool, we'll generate server code.
The oapi-codegen is a command line tool that can be used to generate server code, api client, or HTTP models in Go lang from OpenAPI specification. This tool supports generating server code in different frameworks such as Gin, Chi, Echo, Fiber, gorilla/mux, Iris, net/http. For this guide, we will focus on generating Gin server code.
1, Install the tool
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
Note: Ensure your Go bin directory is added to your system's PATH so that the command is executable.
2. Configure the generation
Create a server_cfg.yml file to tell oapi-codegen how to behave.
package: api
generate:
gin-server: true
strict-server: true
models: true
output: gen.go
This configuration tells oapi-codegen to generate Gin server code and models from the OpenAPI specification. The output field specifies the output file name. Why strict-server? Standard generation gives you basic routing, but you still have to manually unmarshal JSON and validate struct fields. The "Strict Server" mode generates code that handles all the request parsing and response marshaling for you, giving you a type-safe request payload and response body.
3. Run the generator:
oapi-codegen -config server_cfg.yml openapi.yaml
Implementing the server
After running the command, you'll notice a gen.go file in your project directory. This file contains the generated server code. A quick dive into the code, you'll see type structs models generated from the component schema. Aside from the generated structs, you'll also notice that server handlers were generated from the paths defined in the OpenAPI spec. What you should concern yourself with is the StrictServerInterface. This is what you'll need to provide an implementation for writing your business logic.
// StrictServerInterface represents all server handlers.
type StrictServerInterface interface {
// Create a new user account
// (POST /auth/signup)
PostAuthSignup(ctx context.Context, request PostAuthSignupRequestObject) (PostAuthSignupResponseObject, error)
// ... other methods
}
Now, implementing the business logic is straightforward and type-safe:
func (s *Server) PostAuthSignup(ctx context.Context, request api.PostAuthSignupRequestObject) (api.PostAuthSignupResponseObject, error) {
// Payload is already unmarshaled and typed!
payload := request.Body
// Handle payload validation
if payload.Password == "" {
return api.PostAuthSignup400JSONResponse{
BadRequestJSONResponse: api.BadRequestJSONResponse{
Error: "Password is required",
},
}, nil
}
// Handle business logic
user, err := s.userService.CreateUser(ctx, payload)
// Error handling should be more refined
if err != nil {
return api.PostAuthSignup400JSONResponse{
BadRequestJSONResponse: api.BadRequestJSONResponse{
Error: "Failed to create user",
},
}, nil
}
return api.PostAuthSignup201JSONResponse{
Token: "generated_jwt_token",
User: &api.User{
Email: &payload.Email,
Name: &payload.Name,
},
}, nil
}
With this you've eliminated the need to write boilerplate code for request handlers. You only need focus on the business logic implementation.
Note: The oapi-codegen can also be used to generate client code in Go lang.
Generate client code in Dart (Flutter)
For the frontend, we can generate a complete API client using the swagger_dart_code_generator package.
1. Add your spec file
Create a swagger/ directory in your project root and move your OpenAPI specification file into it. The folder path is configurable, it's my preference to keep it at the root.
2. Add dependencies
Add these to your pubspec.yaml:
dev_dependencies:
build_runner: ^2.4.9
chopper_generator: ^8.0.3
json_serializable: ^6.8.0
swagger_dart_code_generator: ^3.0.1
Then run dart pub get or flutter pub get depending on your project type.
3. Configure the builder
Create a build.yaml file in the root of your project, to configure code generation behaviour:
targets:
$default:
sources:
- lib/**
- swagger/**
- $package$
builders:
swagger_dart_code_generator:
options:
input_folder: "swagger/"
output_folder: "lib/src/sources/generated/"
json_serializable:
generate_for:
- lib/src/sources/**
chopper_generator:
generate_for:
- lib/src/sources/generated/openapi.swagger.dart
For a complete list of configuration options and their usage, check the swagger_dart_code_generator documentation.
4. Run the builder:
dart run build_runner build
Using the generated client code
The generator creates a strongly-typed client using the chopper library. You can now make API calls without accidentally mistyping a field name or endpoint URL.
import 'package:dart_client/src/sources/generated/openapi.swagger.dart';
final client = Openapi.create();
final request = SignupRequest(
name: "John Doe",
email: "john.doe@example.com",
password: "password",
);
final response = await client.authSignupPost(request);
if (response.isSuccessful) {
print("User created: ${response.body?.user?.name}");
} else {
print("Error: ${response.error}");
}
The authSignupPost() is a generated method from the OpenAPI spec.
///Create a new user account
Future<chopper.Response<AuthResponse>> authSignupPost({
required SignupRequest? body,
}) {
generatedMapping.putIfAbsent(
AuthResponse,
() => AuthResponse.fromJsonFactory,
);
return _authSignupPost(body: body);
}
Conclusion
By adopting a schema-first workflow, your team will significantly reduce time spent on boilerplate code, freeing you to focus on the product logic that actually matters. The OpenAPI spec becomes your single source of truth, ensuring consistency across the entire development lifecycle. No more "integration hell," no more manual JSON parsing, and no more out-of-date documentation.
If you are product-inclined, you might spot some potential inefficiencies in this flow. What if your teams work in separate repositories? Yes, the backend team can generate server code and the frontend team can generate client code from the same specification, but what happens when the backend team updates the spec and the frontend team doesn't have the latest copy? What happens if the frontend team accidentally modifies their local copy of the spec? This breeds chaos and inconsistency, voiding the very efficiency we're striving for.
How do we solve this? In a monorepo, you can simply have a central directory house the spec that both projects reference. However, if you are working across multiple repositories, you need a more robust solution.
What's Next? In my next article, I'll show you how to automate this entire pipeline using GitHub Actions—triggering code generation automatically for all teams whenever the schema changes.
Discussion
Do you currently generate your client code, or do you prefer writing it manually? Is it about control, or just habit? Let me know your thoughts in the comments!
Connect with me on LinkedIn and X (Twitter) for more tips on efficient software engineering.



