Skip to content

Writing an API Endpoint

Akshay Katyal edited this page Jun 9, 2022 · 1 revision

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).

Types, Input, and Enum

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.

Creating a schema

We define schemas in the schema folder of the graph. The schema we mostly use are two :

Query

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"
    }
  }
}

Mutation

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

Resolvers

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.

Services

We have a package called api where all the business logic goes. Insideapiwe 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)
}

API / Helper

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 NewUseris 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.