-
Notifications
You must be signed in to change notification settings - Fork 0
Writing an API Endpoint
We use GraphQL for writing API EndPoints and all GraphQL related code is in the graph folder. GraphQL Endpoints are different from the traditional form of writing APIs. You can get more context on how different it is from [here](https://www.youtube.com/watch?v=G0flJz7Zbvc&ab_channel=BenAwadBenAwadVerified).
GraphQL has its own type of system that’s used to define the schema of an API.
A typical example of type for person
.
type User {
id: ID!
name: String!
age: Int
gender: Gender // enum
}
and for input to an API, we use input type.
input GetUserInput {
id: ID!
username: String
email: String
}
here “!“ indicate that name is a mandatory or required field and should be passed whenever this API is called.
The enums data type can also be defined in GraphQL to limit the value of a field.
enum Gender {
Male
Female
Other
}
For each model in a service, we have a .graphqls
file where we define all the type, input, and enums for a particular entity like the user, city, etc.
We define schemas in the schema folder of the graph. The schema we mostly use are two :
This kind of similar to what we have traditional GET request.
type Query {
getUser(input: GetUserInput): User
}
Here GetPersonInput and Person
are the same data type define earlier. This query will return a response something like this.
{
"data": {
"getUser": {
"name": "Tom",
"age": "23"
}
}
}
Again this kind of similar to what we have with traditional REST API POST, UPDATE, or DELETE
request. As the name suggests when we trying to create a new or modified existing entity we use a Mutation query.
type Mutation {
createUser(input: NewUserInput): User
}
This will return a response to something similar to what the get query returned.
We define all our query and mutation in a single file inside a graph folder called schema.graphqls
A resolver is a function that's responsible for populating the data for every single field in your schema. So this something lies in files with extension as .resolver.go.
To make our life easy we can generate resolvers using the command ``this in the main folder.
go generate ./...
it will auto-generate the code and setup required to run resolvers. Initially generate resolvers will look something like this.
func (r *userResolver) User(ctx context.Context, obj *graphmodel.User) (*graphmodel.User, error) {
panic(fmt.Errorf("Not Implemented"))
}
Here an error message is sent as a response to a query, but we need to change this manually to the respective backend service.
Once Done with Graphql we need to add services/APIs for performing business logic and repositories to perform actions like CRUD on DB or call other services for inter-service communication. To achieve this we follow a very standard structure.
We have a package called api
where all the business logic goes. Insideapi
we have a service
package where we initialize all the services. below is a typical example
type Services interface {
User() api.User
}
type services struct {
userService api.User
}
func (svc *services) User() api.User {
return svc.userService
}
func Init() Services {
db := instance.DB()
return &services{
userService: api.NewUser(
repository.NewUserRepo(db, validator),
),
}
}
So we have an interface where enclosing all services we created, and Init
Initializes the services struct with all the dependencies required and these initialized instances can be used from anywhere and can be plugged into the resolvers we created earlier.
func (r *mutationResolver) CreateUser(ctx context.Context, input graphmodel.NewUserInput) (*graphmodel.CreateUserResponse, error) {
return r.Services.User().Create(ctx, input)
}
In the API
package, we have a separate file for each real-world entity that requires a business logic or validation before saving it into DB or sending a response from DB. A typical example for an API service:
type User interface {
Create(ctx context.Context, data graphmodel.NewUserInput) (*graphmodel.CreateUserResponse, error)
Get(ctx context.Context, data graphmodel.GetUserInput) (*graphmodel.User, error)
}
type user struct {
userRepo repository.UserRepo
}
// Create is the resolver for creating a user
func (c *user) Create(ctx context.Context, data graphmodel.NewUserInput) (*graphmodel.CreateUserResponse, error) {
// logic for user creation
return u, nil
}
// NewUser is the initialization method for the user resolvers
func NewUser(userRepo repository.UserRepo,)
User {
return &user{
userRepo: userRepo,
}
}
Here NewUser
is a public function called from service we saw earlier, this introduces all dependency need like userRepo
to DB operations like saving user to the database.