Skip to main content

Create an HTTP server

One of the primary use cases for gosoline is to create a REST web service. In this tutorial, you'll do just that!

Specifically, you'll use gosoline to create a web service that handles a single endpoint:

GET /todo?id={ID}&text={TODO TEXT}

This endpoint will:

  1. Accept the data from the request's querystring
  2. Send a Json response with the id and text from the request, along with an ISO-formatted creation date and time.

Here's an example response:

{
"Id": 3,
"Text": "doit",
"CreatedAt": "2023-09-05T11:18:09.325852335+02:00"
}

Before you begin

Before you begin, make sure you have Golang installed on your machine.

Set up your file structure

First, you need to set up the following file structure:

todo/
├── handler.go
├── main.go
└── config.dist.yml

For example, in Unix, run:

mkdir todo; cd todo
touch handler.go
touch main.go
touch config.dist.yml

Those are all the files you need to build your web service with gosoline! Next, you'll implement each of these files, starting with handler.go.

Implement handler.go

In handler.go, add the following code:

handler.go

package main

import (
"context"
"time"

"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/httpserver"
"github.com/justtrackio/gosoline/pkg/log"
)


type Todo struct {
Id int `form:"id"`
Text string `form:"text"`
CreatedAt time.Time
}


type TodoHandler struct {
logger log.Logger
}


func (t TodoHandler) GetInput() interface{} {
return &Todo{}
}


func NewTodoHandler(ctx context.Context, config cfg.Config, logger log.Logger) (*TodoHandler, error) {
handler := &TodoHandler{
logger: logger,
}

return handler, nil
}


func (t TodoHandler) Handle(ctx context.Context, request *httpserver.Request) (*httpserver.Response, error) {
// Initialize a Todo struct from the request body
todo := request.Body.(*Todo)

// Set its CreatedAt to now
todo.CreatedAt = time.Now()

// Log the request using the TodoHandler struct's logger
t.logger.Info("got todo with id %d", todo.Id)

// Return a Json response object with information from the Todo struct
return httpserver.NewJsonResponse(todo), nil
}

Now, you'll walkthrough this file in detail to learn how it works.

Import dependencies

At the top of handler.go, you declared the package and imported some dependencies:

handler.go
package main

import (
"context"
"time"

"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/httpserver"
"github.com/justtrackio/gosoline/pkg/log"
)

Here, you declared the package as main. Then, you imported the context and time modules along with three gosoline dependencies:

Define a Todo

Next, you created a Todo struct:

handler.go
type Todo struct {
Id int `form:"id"`
Text string `form:"text"`
CreatedAt time.Time
}

Later in this file, you'll use this to bind the data from the HTTP querystring. Referring back to the specification for your todo service, the expected querystring looks like this:

?id={ID}&text={TODO TEXT}

So, in your struct, you have an Id and a Text. With the form: tag, you've specified the querystring parameter from which to pull the value for these keys.

Create a TodoHandler

Next, you created a struct for handling todos:

handler.go
type TodoHandler struct {
logger log.Logger
}

This is a simple structure that holds a log.Logger reference.

Get request inputs

Next, you created a function called GetInput() that returns the input instance to use:

handler.go
func (t TodoHandler) GetInput() interface{} {
return &Todo{}
}

This is required because your TodoHandler must implement the GetInput() method of the httpserver.HandlerWithInput interface.

Handle new todos

Next, you created a function called NewTodoHandler():

handler.go
func NewTodoHandler(ctx context.Context, config cfg.Config, logger log.Logger) (*TodoHandler, error) {
handler := &TodoHandler{
logger: logger,
}

return handler, nil
}

The config and logger argument types come from gosoline. This function initializes a new TodoHandler and assigns it the logger. Then, it returns the handler with no error.

Handle requests

Finally, you created a Handle() function that accepts a context.Context and an httpserver.Request (also from gosoline) and handles that request:

handler.go
func (t TodoHandler) Handle(ctx context.Context, request *httpserver.Request) (*httpserver.Response, error) {
// Initialize a Todo struct from the request body
todo := request.Body.(*Todo)

// Set its CreatedAt to now
todo.CreatedAt = time.Now()

// Log the request using the TodoHandler struct's logger
t.logger.Info("got todo with id %d", todo.Id)

// Return a Json response object with information from the Todo struct
return httpserver.NewJsonResponse(todo), nil
}

Here, you handle requests and responses in just a few lines of code. handler.go does most of the heavy lifting for your web service. However, you still need a main entry point to your service, where you'll make use of the logic in handler.go. This, you'll add in main.go.

Implement main.go

In main.go, add the following code:

main.go

package main

import (
"context"
"fmt"

"github.com/justtrackio/gosoline/pkg/application"
"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/httpserver"
"github.com/justtrackio/gosoline/pkg/log"
)


func main() {
// Initialize an HTTP server factory that defines your HTTP route
definer := func(ctx context.Context, config cfg.Config, logger log.Logger) (*httpserver.Definitions, error) {
// Initialize a reference to httpserver.Definitions, which you use to create a GET route
def := &httpserver.Definitions{}

// Instantiate two new variables for handling errors and request input data
var err error
var handler httpserver.HandlerWithInput

// Create a handler (`NewTodoHandler`) that handles the request input data.
// If there is an error, return an error message.
// If there is no error, assign this `NewTodoHandler` to the `handler` variable from the last step.
if handler, err = NewTodoHandler(ctx, config, logger); err != nil {
return nil, fmt.Errorf("can not create trip handler: %w", err)
}

// Create a GET route for the endpoint /todo, using the handler
def.GET("/todo", httpserver.CreateQueryHandler(handler))

// Return the response from handler
return def, nil
}

// Run an HTTP server application based on the logic from the previous steps
application.RunHttpDefaultServer(definer)
}

Now, you'll walkthrough this file in detail to learn how it works.

Import dependencies

In main.go, you declared the package and imported some dependencies:

main.go
package main

import (
"context"
"fmt"

"github.com/justtrackio/gosoline/pkg/application"
"github.com/justtrackio/gosoline/pkg/cfg"
"github.com/justtrackio/gosoline/pkg/httpserver"
"github.com/justtrackio/gosoline/pkg/log"
)

Here, you declared the package as main. Then, you imported the context and time modules along with four gosoline dependencies:

Create your main entry point

Next, you created your main() function as the entry point to your service:

main.go
func main() {
// Initialize an HTTP server factory that defines your HTTP route
definer := func(ctx context.Context, config cfg.Config, logger log.Logger) (*httpserver.Definitions, error) {
// Initialize a reference to httpserver.Definitions, which you use to create a GET route
def := &httpserver.Definitions{}

// Instantiate two new variables for handling errors and request input data
var err error
var handler httpserver.HandlerWithInput

// Create a handler (`NewTodoHandler`) that handles the request input data.
// If there is an error, return an error message.
// If there is no error, assign this `NewTodoHandler` to the `handler` variable from the last step.
if handler, err = NewTodoHandler(ctx, config, logger); err != nil {
return nil, fmt.Errorf("can not create trip handler: %w", err)
}

// Create a GET route for the endpoint /todo, using the handler
def.GET("/todo", httpserver.CreateQueryHandler(handler))

// Return the response from handler
return def, nil
}

// Run an HTTP server application based on the logic from the previous steps
application.RunHttpDefaultServer(definer)
}

main() puts together all the data structures and logic from handler.go into a single, coherent function.

Now, you've implemented your server's main entry point and handler logic. Next, you'll configure your server.

Configure your server

In config.dist.yml, configure your server:

config.dist.yml
env: dev
app_project: gosoline
app_family: get-started
app_group: http-server
app_name: example

Here, you set some minimal configurations for your web server. The final step is to test it to confirm that it works as expected.

Test your gosoline server

From the todo directory, start your go module, install the dependencies, and run your server:

go mod init todo/m
go mod tidy
go run .

In your console, you'll see a log stream that indicates your server is running properly. By default, the server is exposed to localhost:8080.

Make a GET request to localhost:8080/todo. For example:

curl 'localhost:8080/todo?id=1&text=do_it'

You'll get the following output:

{"Id":1,"Text":"do_it","CreatedAt":"2023-09-05T16:55:49.02692+02:00"}

Conclusion

That's it! You created your first gosoline web service. In this tutorial, you were able to:

  • Handle requests
  • Send Json-serialized responses
  • Log information
  • And more...

Check out these resources to learn more about creating web services with gosoline: