This post is published here. The dev.to post won't update regularly so to see the newest tutorial visit the link.
Table Of Contents
- Table Of Contents
Motivation
Go is a modern general purpose programming language designed by google; best known for it's simplicity, concurrency and fast performance. It's being used by big players in the industry like Google, Docker, Lyft and Uber. If you are new to golang you can start from golang tour to learn fundamentals.
gqlgen is a library for creating GraphQL applications in Go.
In this tutorial we Implement a Hackernews GraphQL API clone with golang and gqlgen and learn about GraphQL fundamentals along the way.
Source code and also this tutorial are available on Github at: https://github.com/Glyphack/go-graphql-hackernews
What is a GraphQL server?
A GraphQL server is able to receive requests in GraphQL Query Language format and return response in desired form.
GraphQL is a query language for API so you can send queries and ask for what you need and exactly get that piece of data.
In this sample query we are looking for address, title of the links and name of the user who add it:
query {
links{
title
address,
user{
name
}
}
}
response:
{
"data": {
"links": [
{
"title": "our dummy link",
"address": "https://address.org",
"user": {
"name": "admin"
}
}
]
}
}
Schema-Driven Development
In GraphQL your API starts with a schema that defines all your types, queries and mutations, It helps others to understand your API. So it's like a contract between server and the client.
Whenever you need to add a new capability to a GraphQL API you must redefine schema file and then implement that part in your code. GraphQL has it's Schema Definition Language for this purpose.
gqlgen is a Go library for building GraphQL servers and has a nice feature that generates code based on your schema definition.
Getting started
In this tutorial we are going to create a Hackernews clone with Go and gqlgen, So our API will be able to handle registration, authentication, submitting links and getting list of links.
Project Setup
Create a directory for project and initialize go modules file:
go mod init github.com/[username]/hackernews
after that use gqlgen init
command to setup a gqlgen project.
go run github.com/99designs/gqlgen init
Here is a description from gqlgen about the generated files:
-
gqlgen.yml
— The gqlgen config file, knobs for controlling the generated code. -
generated.go
— The GraphQL execution runtime, the bulk of the generated code. -
models_gen.go
— Generated models required to build the graph. Often you will override these with your own models. Still very useful for input types. -
resolver.go
— This is where your application code lives. generated.go will call into this to get the data the user has requested. -
server/server.go
— This is a minimal entry point that sets up an http.Handler to the generated GraphQL server. start the server withgo run server.go
and open your browser and you should see the graphql playground, So setup is right!
Defining Our Schema
Now let's start with defining schema we need for our API.
We have two types Link and User each of them for representing Link and User to client, a links
Query to return list of Links. an input for creating new links and mutation for creating link. we also need mutations to for auth system which includes Login, createUser, refreshToken(I'll explain them later) then run the command below to regenerate graphql models.
type Link {
id: ID!
title: String!
address: String!
user: User!
}
type User {
id: ID!
name: String!
}
type Query {
links: [Link!]!
}
input NewLink {
title: String!
address: String!
}
input RefreshTokenInput{
token: String!
}
input NewUser {
username: String!
password: String!
}
input Login {
username: String!
password: String!
}
type Mutation {
createLink(input: NewLink!): Link!
createUser(input: NewUser!): String!
login(input: Login!): String!
# we'll talk about this in authentication section
refreshToken(input: RefreshTokenInput!): String!
}
Now remove resolver.go and re-run the command to regenerate files;
rm resolver.go
go run github.com/99designs/gqlgen
After gqlgen generated code for us with have to implement our schema, we do that in resolver.go
, as you see there is functions for Queries and Mutations we defined in our schema.
Queries
In the previous section we setup up the server, Now we try to implement a Query that we defined in schema.grpahql
.
What Is A Query
a query in graphql is asking for data, you use a query and specify what you want and graphql will return it back to you.
Simple Query
open resolver.go
file and take a look at Links function,
func (r *queryResolver) Links(ctx context.Context) ([]*Link, error) {
Notice that this function takes a Context and returns slice of Links and an error(is there is any).
ctx argument contains the data from the person who sends request like which user is working with app(we'll see how later), etc.
Let's make a dummy response for this function, for now.
resolver.go
:
func (r *queryResolver) Links(ctx context.Context) ([]*Link, error) {
var links []*Link
links = append(links, &Link{Title: "our dummy link", Address: "https://address.org", User: &User{Username: "admin"}})
return links, nil
}
now run the server with go run server/server.go
and send this query in graphiql:
query {
links{
title
address,
user{
name
}
}
}
And you will get:
{
"data": {
"links": [
{
"title": "our dummy link",
"address": "https://address.org",
"user": {
"name": "admin"
}
}
]
}
}
Now you know how we generate response for our graphql server. But this response is just a dummy response we want be able to query all other users links, In the next section we setup database for our app to be able to save links and retrieve them from database.
Mutations
What Is A Mutation
Simply mutations are just like queries but they can cause a data write, Technically Queries can be used to write data too however it's not suggested to use it.
So mutations are like queries, they have names, parameters and they can return data.
A Simple Mutation
Let's try to implement the createLink mutation, since we do not have a database set up yet(we'll get it done in the next section) we just receive the link data and construct a link object and send it back for response!
Open resolver.go
and Look at CreateLink
function:
func (r *mutationResolver) CreateLink(ctx context.Context, input NewLink) (*Link, error) {
This function receives a NewLink
with type of input
we defined NewLink structure in our schema.graphql
try to look at the structure and try Construct a Link
object that be defined in our schema.ghraphql
:
func (r *mutationResolver) CreateLink(ctx context.Context, input NewLink) (*Link, error) {
var link Link
var user User
link.Address = input.Address
link.Title = input.Title
user.Username = "test"
link.User = &user
return &link, nil
}
now run server and use the mutation to create a new link:
mutation {
createLink(input: {title: "new link", address:"http://address.org"}){
title,
user{
name
}
address
}
}
and you will get:
{
"data": {
"createLink": {
"title": "new link",
"user": {
"name": "test"
},
"address": "http://address.org"
}
}
}
Nice now we know what are mutations and queries we can setup our database and make these implementations more practical.
Database
Before we jump into implementing GraphQL schema we need to setup database to save users and links, This is not supposed to be tutorial about databases in go but here is what we are going to do:
- setup MySQL
- define our models and create migrations
Setup MySQL
If you have docker you can run Mysql image from docker and use it.
docker run --name mysql -e MYSQL_ROOT_PASSWORD=dbpass -d mysql:latest
now run docker ps
and you should see our mysql image is running:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8fea71529bb2 mysql:latest "docker-entrypoint.s…" 2 hours ago Up 2 hours 3306/tcp, 33060/tcp mysql
Now create a database for our application:
docker exec -it mysql bash
mysql -u root -p
CREATE DATABASE hackernews;
Models and migrations
We need to create migrations for our app so every time our app runs it creates tables it needs to work properly, we are going to use golang-migrate package.
create a folder structure for our database files:
go-graphql-hackernews
--internal
---------pkg
-----------db
-------------migrations
---------------mysql
Install go mysql driver and golang-migrate packages then create migrations:
go get -u github.com/go-sql-driver/mysql
go build -tags 'mysql' -ldflags="-X main.Version=$(git describe --tags)" -o $GOPATH/bin/migrate github.com/golang-migrate/migrate/cmd/migrate
cd internal/pkg/db/migrations/
migrate create -ext sql -dir mysql -seq create_users_table
migrate create -ext sql -dir mysql -seq create_links_table
migrate command will create two files for each migration ending with .up and .down; up is responsible for applying migration and down is responsible for reversing it.
open create_users_table.up.sql
and add table for our users:
CREATE TABLE IF NOT EXISTS Users(
ID INT NOT NULL UNIQUE AUTO_INCREMENT,
Username VARCHAR (127) NOT NULL UNIQUE,
Password VARCHAR (127) NOT NULL,
PRIMARY KEY (ID)
)
in create_links_table.up.sql
:
CREATE TABLE IF NOT EXISTS Links(
ID INT NOT NULL UNIQUE AUTO_INCREMENT,
Title VARCHAR (255) ,
Address VARCHAR (255) ,
UserID INT ,
FOREIGN KEY (UserID) REFERENCES Users(ID) ,
PRIMARY KEY (ID)
)
We need one table for saving links and one table for saving users, Then we apply these to our database using migrate command.
migrate -database mysql://root:dbpass@(172.17.0.2:3306)/hackernews -path internal/pkg/db/migrations/mysql up
Last thing is that we need a connection to our database, for this we create a mysql.go under mysql folder(We name this file after mysql since we are now using mysql and if we want to have multiple databases we can add other folders) with a function to initialize connection to database for later use.
internal/pkg/db/mysql/mysql.go
:
package database
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"github.com/golang-migrate/migrate"
"github.com/golang-migrate/migrate/database/mysql"
_ "github.com/golang-migrate/migrate/source/file"
"log"
)
var Db *sql.DB
func InitDB() {
db, err := sql.Open("mysql", "root:dbpass@(172.17.0.2:3306)/hackernews")
if err != nil {
log.Panic(err)
}
if err = db.Ping(); err != nil {
log.Panic(err)
}
Db = db
}
func Migrate() {
if err := Db.Ping(); err != nil {
log.Fatal(err)
}
driver, _ := mysql.WithInstance(Db, &mysql.Config{})
m, _ := migrate.NewWithDatabaseInstance(
"file://internal/pkg/db/migrations/mysql",
"mysql",
driver,
)
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
log.Fatal(err)
}
}
InitDB
Function creates a connection to our database and Migrate
function runs migrations file for us.
In Migrate
function we apply migrations just like we did with command line but with this function your app will always apply the latest migrations before start.
Then call InitDB
and Migrate
(Optional) In main func to create database connection at the start of the app:
func main() {
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
database.InitDB()
database.Migration()
http.Handle("/", handler.Playground("GraphQL playground", "/query"))
http.Handle("/query", handler.GraphQL(hackernews.NewExecutableSchema(hackernews.Config{Resolvers: &hackernews.Resolver{}})))
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
Create and Retrieve Links
Now we have our database ready we can start implementing our schema!
CreateLinks
Lets implement CreateLink mutation; first we need a function to let us write a link to database.
Create a folders links and users inside internal folder, these packages are layers between database and our app.
internal/users/users.go
:
package users
type User struct {
ID string `json:"id"`
Username string `json:"name"`
Password string `json:"password"`
}
internal/links/links.go
:
package links
import (
database "github.com/glyphack/go-graphql-hackernews/internal/pkg/db/mysql"
"github.com/glyphack/go-graphql-hackernews/internal/users"
"log"
)
// #1
type Link struct {
ID string
Title string
Address string
User *users.User
}
//#2
func (link Link) Save() int64 {
//#3
statement, err := database.Db.Prepare("INSERT INTO Links(Title,Address) VALUES(?,?)")
if err != nil {
log.Fatal(err)
}
//#4
res, err := statement.Exec(link.Title, link.Address)
if err != nil {
log.Fatal(err)
}
//#5
id, err := res.LastInsertId()
if err != nil {
log.Fatal("Error:", err.Error())
}
log.Print("Row inserted!")
return id
}
In users.go we just defined a struct
that represent users we get from database, But let me explain links.go part by part:
- 1: definition of struct that represent a link.
- 2: function that insert a Link object into database and returns it's ID.
- 3: our sql query to insert link into Links table. you see we used prepare here before db.Exec, the prepared statements helps you with security and also performance improvement in some cases. you can read more about it here.
- 4: execution of our sql statement.
- 5: retrieving Id of inserted Link.
Now we use this function in our CreateLink resolver:
resolver.go
:
func (r *mutationResolver) CreateLink(ctx context.Context, input NewLink) (*Link, error) {
var link links.Link
link.Title = input.Title
link.Address = input.Address
linkId := link.Save()
return &Link{ID: strconv.FormatInt(linkId, 10), Title:link.Title, Address:link.Address}, nil
}
Hopefully you understand this piece of code, we create a link object from input and save it to database then return newly created link(notice that we convert the ID to string with strconv.FormatInt
).
note that here we have 2 structs for Link in our project, one is use for our graphql server and one is for our database.
run the server and open graphiql page to test what we just wrote:
mutation create{
createLink(input: {title: "something", address: "somewhere"}){
title,
address,
id,
}
}
{
"data": {
"createLink": {
"title": "something",
"address": "somewhere",
"id": "1"
}
}
}
Grate job!
links Query
Just like how we implemented CreateLink mutation we implement links query, we need a function to retrieve links from database and pass it to graphql server in our resolver.
Create a function named GetAll
internal/links/links.go
:
func GetAll() []Link {
stmt, err := database.Db.Prepare("select id, title, address from Links")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query()
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var links []Link
for rows.Next() {
var link Link
err := rows.Scan(&link.ID, &link.Title, &link.Address)
if err != nil{
log.Fatal(err)
}
links = append(links, link)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
return links
}
Return links from GetAll in Links query.
resolver.go
:
func (r *queryResolver) Links(ctx context.Context) ([]*Link, error) {
var resultLinks []*Link
var dbLinks []links.Link
dbLinks = links.GetAll()
for _, link := range dbLinks{
resultLinks = append(resultLinks, &Link{ID:link.ID, Title:link.Title, Address:link.Address})
}
return resultLinks, nil
}
Now query Links at graphiql:
query {
links {
title
address
id
}
}
result:
{
"data": {
"links": [
{
"title": "something",
"address": "somewhere",
"id": "1"
}
]
}
}
Authentication
One of most common layers in web applications is authentication system, our app is no exception. For authentication we are going to use jwt tokens as our way to authentication users, lets see how it works.
JWT
JWT or Json Web Token is a string containing a hash that helps us verify who is using application. Every token is constructed of 3 parts like xxxxx.yyyyy.zzzzz
and name of these parts are: Header, Payload and Signature. Explanation about these parts are more about JWT than our application you can read more about them here.
whenever a user login to an app server generates a token for user, Usually server saves some information like username about the user in token to be able to recognize the user later using that token.This tokens get signed by a key so only the issuer app can reopen the token.
We are going to implement this behavior in our app.
Setup
In our app we need to be able to generate a token for users when they sign up or login and a middleware to authenticate users by the given token, then in our views we can know the user interacting with app. We will be using github.com/dgrijalva/jwt-go
library to generate and prase JWT tokens.
Generating and Parsing JWT Tokens
We create a new directory pkg in the root of our application, you have seen that we used internal for what we want to only be internally used withing our app, pkg directory is for files that we don't mind if some outer code imports it into itself and generation and validation jwt tokens are this kinds of code.
There is a concept named claims it's not only limited to JWT We'll see more about it in rest of the section.
pkg/jwt/jwt.go
:
package jwt
import (
"github.com/dgrijalva/jwt-go"
"log"
"time"
)
// secret key being used to sign tokens
var (
SecretKey = []byte("secret")
)
//GenerateToken generates a jwt token and assign a username to it's claims and return it
func GenerateToken(username string) (string, error) {
token := jwt.New(jwt.SigningMethodHS256)
/* Create a map to store our claims */
claims := token.Claims.(jwt.MapClaims)
/* Set token claims */
claims["username"] = username
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()
tokenString, err := token.SignedString(SecretKey)
if err != nil {
log.Fatal("Error in Generating key")
return "", err
}
return tokenString, nil
}
//ParseToken parses a jwt token and returns the username it it's claims
func ParseToken(tokenStr string) (string, error) {
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
return SecretKey, nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
username := claims["username"].(string)
return username, nil
} else {
return "", err
}
}
Let's talk about what above code does:
- GenerateToken function is going to be used whenever we want to generate a token for user, we save username in token claims and set token expire time to 5 minutes later also in claims.
- ParseToken function is going to be used whenever we receive a token and want to know who sent this token.
User SignUp and Login Functionality
Til now we can generate a token for each user but before generating token for every user, we need to assure user exists in our database. Simply we need to query database to match the user with given username and password.
Another thing is when a user tries to register we insert username and password in our database.
internal/users/users.go
:
package users
import (
"database/sql"
"github.com/glyphack/go-graphql-hackernews/internal/pkg/db/mysql"
"golang.org/x/crypto/bcrypt"
"log"
)
type User struct {
ID string `json:"id"`
Username string `json:"name"`
Password string `json:"password"`
}
func (user *User) Create() {
statement, err := database.Db.Prepare("INSERT INTO Users(Username,Password) VALUES(?,?)")
print(statement)
if err != nil {
log.Fatal(err)
}
hashedPassword, err := HashPassword(user.Password)
_, err = statement.Exec(user.Username, hashedPassword)
if err != nil {
log.Fatal(err)
}
}
//HashPassword hashes given password
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
//CheckPassword hash compares raw password with it's hashed values
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
The Create function is much like the CreateLink function we saw earlier but let's break down the Authenticate code:
- first we have a query to select password from users table where username is equal to the username we got from resolver.
- We use QueryRow instead of Exec we used earlier; the difference is
QueryRow()
will return a pointer to asql.Row
. - Using
.Scan
method we fill the hashedPassword variable with the hashed password from database. Obviously you don't want to save raw passwords in your database. - then we check if any user with given username exists or not, if there is not any we return
false
, and if we found any we check the user hashedPassword with the raw password given.(Notice that we save hashed passwords not raw passwords in database in line 23)
In the next part we set the tools we have together to detect the user that is using the app.
Authentication Middleware
Every time a request comes to our resolver before sending it to resolver we want to recognize the user sending request, for this purpose we have to write a code before every resolver, but using middleware we can have a auth middleware that executes before request send to resolver and does the authentication process. to read more about middlewares visit.
internal/users/users.go
:
//GetUserIdByUsername check if a user exists in database by given username
func GetUserIdByUsername(username string) (int, error) {
statement, err := database.Db.Prepare("select ID from Users WHERE Username = ?")
if err != nil {
log.Fatal(err)
}
row := statement.QueryRow(username)
var Id int
err = row.Scan(&Id)
if err != nil {
if err != sql.ErrNoRows {
log.Print(err)
}
return 0, err
}
return Id, nil
}
We use this function to get user object with username in authentication middeware.
And now let's create our auth middleware, for more information visit gql authentication docs.
internal/auth/middleware.go
:
package auth
import (
"context"
"net/http"
"strconv"
"github.com/glyphack/go-graphql-hackernews/internal/users"
"github.com/glyphack/go-graphql-hackernews/pkg/jwt"
)
var userCtxKey = &contextKey{"user"}
type contextKey struct {
name string
}
func Middleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("token")
// Allow unauthenticated users in
if err != nil || c == nil {
next.ServeHTTP(w, r)
return
}
//validate jwt token
tokenStr := c.Value
username, err := jwt.ParseToken(tokenStr)
if err != nil {
http.Error(w, "Invalid token", http.StatusForbidden)
return
}
// create user and check if user exists in db
user := users.User{Username: username}
id, err := users.GetUserIdByUsername(username)
if err != nil {
next.ServeHTTP(w, r)
return
}
user.ID = strconv.Itoa(id)
// put it in context
ctx := context.WithValue(r.Context(), userCtxKey, user)
// and call the next with our new context
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
// ForContext finds the user from the context. REQUIRES Middleware to have run.
func ForContext(ctx context.Context) *users.User {
raw, _ := ctx.Value(userCtxKey).(*users.User)
return raw
}
Now we use the middleware we declared in our server:
server/server.go
:
package main
import (
"github.com/glyphack/go-graphql-hackernews/internal/auth"
"log"
"net/http"
"os"
"github.com/99designs/gqlgen/handler"
hackernews "github.com/glyphack/go-graphql-hackernews"
"github.com/glyphack/go-graphql-hackernews/internal/pkg/db/mysql"
"github.com/go-chi/chi"
)
const defaultPort = "8080"
func main() {
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
router := chi.NewRouter()
router.Use(auth.Middleware())
database.InitDB()
database.Migrate()
server := handler.GraphQL(hackernews.NewExecutableSchema(hackernews.Config{Resolvers: &hackernews.Resolver{}}))
router.Handle("/", handler.Playground("GraphQL playground", "/query"))
router.Handle("/query", server)
log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
log.Fatal(http.ListenAndServe(":"+port, router))
}
Continue Implementing schema
Now that we have working authentication system we can get back to implementing our schema.
CreateUser
We continue our implementation of CreateUser mutation with functions we have written in auth section.
resolver.go
:
func (r *mutationResolver) CreateUser(ctx context.Context, input NewUser) (string, error) {
var user users.User
user.Username = input.Username
user.Password = input.Password
err := user.Create()
token, err := jwt.GenerateToken(user.Username)
if err != nil{
return "", err
}
return token, nil
}
In our mutation first we create a user using given username and password and then generate a token for the user so we can recognize the user in requests.
Start the server and try it in graphiql:
query:
mutation {
createUser(input: {username: "user1", password: "123"})
}
results:
{
"data": {
"createUser": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODE0NjAwODUsImlhdCI6MTU4MTQ1OTc4NX0.rYLOM123kSulGjvK5VP8c7S0kgk03WweS2VJUUbAgNA"
}
}
So we successfully created our first user!
Login
For this mutation, first we have to check if user exists in database and given password is correct, then we generate a token for user and give it bach to user.
internal/users.go
:
func (user *User) Authenticate() bool {
statement, err := database.Db.Prepare("select Password from Users WHERE Username = ?")
if err != nil {
log.Fatal(err)
}
row := statement.QueryRow(user.Username)
var hashedPassword string
err = row.Scan(&hashedPassword)
if err != nil {
if err == sql.ErrNoRows {
return false
} else {
log.Fatal(err)
}
}
return CheckPasswordHash(user.Password, hashedPassword)
}
//CheckPassword hash compares raw password with it's hashed values
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
Explanation:
- we select the user with the given username and then check if hash of the given password is equal to hashed password that we saved in database.
resolver.go
func (r *mutationResolver) Login(ctx context.Context, input Login) (string, error) {
var user users.User
user.Username = input.Username
user.Password = input.Password
correct := user.Authenticate()
if !correct {
// 1
return "", &users.WrongUsernameOrPasswordError{}
}
token, err := jwt.GenerateToken(user.Username)
if err != nil{
return "", err
}
return token, nil
}
We used the Authenticate function declared above and after that if the username and password are correct we return a new token for user and if not we return error, &users.WrongUsernameOrPasswordError
, here is implementation for this error:
internal/users/errors.go
:
package users
type WrongUsernameOrPasswordError struct{}
func (m *WrongUsernameOrPasswordError) Error() string {
return "wrong username or password"
}
To define a custom error in go you need a struct with Error method implemented, here is our error for wrong username or password with it's Error() method.
Again you can try login with username and password from the user we created and get a token.
Refresh Token
This is the last endpoint we need to complete our authentication system, imagine a user has loggedIn in our app and it's token is going to get expired after minutes we set(when generated the token), now we need a solution to keep our user loggedIn. One solution is to have a endpoint to get tokens that are going to expire and regenerate a new token for that user so that app uses new token.
So our endpoint should take a token, Parse the username and generate a token for that username.
resolver.go
:
func (r *mutationResolver) RefreshToken(ctx context.Context, input RefreshTokenInput) (string, error) {
username, err := jwt.ParseToken(input.Token)
if err != nil {
return "", fmt.Errorf("access denied")
}
token, err := jwt.GenerateToken(username)
if err != nil {
return "", err
}
return token, nil
}
Implementation is pretty straightforward so we skip the explanation for this.
Completing Our app
Our CreateLink mutation left incomplete because we could not authorize users back then, so let's get back to it and complete the implementation.
With what we did in authentication middleware we can retrieve user in resolvers using ctx argument. so in CreateLink function add these lines:
resolver.go
:
func (r *mutationResolver) CreateLink(ctx context.Context, input NewLink) (*Link, error) {
// 1
user := auth.ForContext(ctx)
if user == nil {
return &Link{}, fmt.Errorf("access denied")
}
.
.
.
// 2
link.User = user
linkId := link.Save()
grahpqlUser := &User{
ID: user.ID,
Name: user.Username,
}
return &Link{ID: strconv.FormatInt(linkId, 10), Title:link.Title, Address:link.Address, User:grahpqlUser}, nil
}
Explanation:
- 1: we get user object from ctx and if user is not set we return error with message access denied.
- 2: then we set user of that link equal to the user is requesting to create the link.
And edit the links query to get user from db too.
resolver.go
:
func (r *queryResolver) Links(ctx context.Context) ([]*Link, error) {
var resultLinks []*Link
var dbLinks []links.Link
dbLinks = links.GetAll()
for _, link := range dbLinks{
grahpqlUser := &User{
ID: link.User.ID,
Name: link.User.Username,
}
resultLinks = append(resultLinks, &Link{ID:link.ID, Title:link.Title, Address:link.Address, User:grahpqlUser})
}
return resultLinks, nil
}
The part that is left here is our database operation for creating link, We need to create foreign key from the link we inserting to that user.
internal/links/links.go
:
In our Save method from links changed the query statement to:
statement, err := database.Db.Prepare("INSERT INTO Links(Title,Address, UserID) VALUES(?,?, ?)")
and the line that we execute query to:
res, err := statement.Exec(link.Title, link.Address, link.User.ID)
Then when we query for users we also fill the User
field for Link, so we need to join Links and Users table in our GetAll
functions to fill the User field.
If you are not familiar with join checkout this link.
internal/links/links.go
:
func GetAll() []Link {
stmt, err := database.Db.Prepare("select L.id, L.title, L.address, L.UserID, U.Username from Links L inner join Users U on L.UserID = U.ID") // changed
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query()
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var links []Link
var username string
var id string
for rows.Next() {
var link Link
err := rows.Scan(&link.ID, &link.Title, &link.Address, &id, &username) // changed
if err != nil{
log.Fatal(err)
}
link.User = &users.User{
ID: id,
Username: username,
} // changed
links = append(links, link)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
return links
}
and Our app is finally complete.
Summary
Congratulations on make it to here! You've learned about gqlgen library and some Graphql fundamentals. By implementing a HackerNews clone you've learned about queries, mutations, authentication and GraphQL query language.
Further Steps
If you want to create more complex GrahpQL APIs there are few things you can check out:
- Implement voting feature and pagination for the app.
- Read about Relays.
- don't forget to visit (Learn GraphQL page)[https://graphql.org/learn/] to learn more about GraphQL world.
Top comments (8)
Hi sorry for my late answer but dev.to notifications are kind of mixed up and I missed this one. To be honest I don't know enough to answer your question. you can try stackoverflow. Also I read more on this and give you update on it.
Hey!
I would like to say thank you for sharing
But i found out that your tutorial has some typing errors
Your demo code for Links query :Links()
Also i realize that many tutorials on this dev.to platform have got so many mistyping like you.
Could you please take a second to review ?
Hi, thanks for the feedback. I tried to fix the typing errors before publishing it but since it was huge it still has mistyping problems :) . the source of this tutorial is on github.
github.com/Glyphack/go-graphql-hac...
if you know any other errors you can create a pull request.
Hi, I could not find the miss typing you said. could you give a hint?
Thanks for sharing, really a great example!
As you just get all the fields from the database, is there any libraries which can turn GraphQL queries into SQL sentences based on the queried fields and condition?
There is a way to do this. actually I don't thinks it's the library job to do the thing but you got a point there. gqlgen supports this by giving you the requested fields. here is the relative MR: github.com/99designs/gqlgen/pull/65. I think This is a good topic to extend the tutorial :). Thanks.
Hey shayegan, thank you for sharing this , it was perfect I want to use it for client react app , I'm new in golang , I can't fix cors error , I read gqlgen document and I did every thing base on document , but its not working
Hi, Which version of this tutorial did you used? because It's being maintained in howtographql
github.com/howtographql/howtograph...
howtographql.com/graphql-go/0-intr...
feel free to open issues if you still can't get it to work.