In today’s dynamic development environment, businesses need to reduce time to market while maintaining platform stability and keeping costs low. By using frameworks like Encore, companies can speed up their cloud-native application development without compromising on stability or scalability. Add to a powerful tool like Maroto, and we can deliver production-ready services, such as a PDF invoice generator.

This post is going to show how combining Encore and Maroto helps our teams build a functional invoice generator service with minimal overhead. Encore’s integrated system reduces platform costs by taking care of infrastructure and deployment out of the box, while Maroto simplifies complex PDF creation, allowing our teams to focus on feature development. This results in quicker product iterations and a more stable platform, all without sacrificing team efficiency or development velocity.

By the end of this post, we will have a fully deployable invoice generator API that not only meets business needs but also provides a stable foundation for future product development.

Why Encore?

Encore addresses the common challenges developers face when working with cloud services, which often introduce unnecessary complexity and repetitive time-consuming tasks. By optimizing development processes, Encore helps developers focus on their core objectives, whether launching a new app, migrating to the cloud, or transitioning from monoliths to microservices. It also ensures standardization, security, and better system understanding with features like automated architecture diagrams, API docs, and distributed tracing, making cloud backend development more efficient and secure.

encore arch

Taken from A simplified cloud backend development workflow.

Encore offers a simple solution for backend development, covering all the path from local development and testing to managing cloud infrastructure and DevOps.

PDF Invoice Generator Service

Encore will help us to build a service with only one responsibility, generate PDF invoices based on the provided information. By integrating Encore with Maroto V2, an open-source Golang package to create PDFs, we will expedite our ability to craft invoices while benefiting from hosting this as a separate service, leveraging the scalability, reusability and flexibility of running it this way. Following this approach, our service can be adapted or extended accordingly, without harming the operation of other services. Let’s configure our project.

Setting Up the Project

Installing the Encore CLI to develop our app in a local environment is pretty simple, we can follow their installation process here, or if we have a Mac and Homebrew installed, we can run:

$ brew install encoredev/tap/encore

Now, we will continue by creating an Encore project. The Encore CLI facilitates the process of setting up and deploying cloud-native apps. Run the following command to create a new Encore project:

$ encore app create invoice-generator

After running this, we will be prompted to select the language we wish to use. Currently, Encore offers the option to build our services with Go or Typescript. For this post, we will choose Go.

encore app language

Then, it will show a few example apps, we will choose the Empty app to start from scratch.

encore app template

Next, let’s go to our project and add Maroto. In this post, we will use the V2 of Maroto, which is 2x faster than V1 and offers more usability. To install it we need to run:

$ go get github.com/johnfercher/maroto/v2@v2.1.2

We are all set with the settings, now let’s get down to designing our invoice.

Designing the Invoice with Maroto

Maroto employs a flexible grid-based layout system to simplify PDF generator in Go. Its design revolves around a 12-unit grid system, where columns define the horizontal space for content placement, with each column’s width determining how much of the page it occupies. Using this grid helps us to structure components like text and images into layouts that are well-aligned and responsive. Columns are key containers for content, and Maroto ensures that the sum of column widths in a row does not exceed the total grid width, promoting clean and organized layouts.

size := 12
col := col.New(size)

Rows in Maroto handle vertical structuring and define the height of each section of content. Unlike columns, which use the grid system, rows are created based on millimeters, offering precise control over the vertical space allocated to each row. The layout flow is sequential, with rows stacked on top of each other, allowing for flexible combinations of columns within each row. This structure enables Maroto to support various layout patterns, making it adaptable for different document designs.

row.New(20).Add(
	col.New(6).Add(
		// ...
	),
	col.New(6).Add(
		// ...
	),
)

Lastly, in Maroto V2 everything is treated as a component, whether it’s a row, column, or image, making the system highly modular and flexible. This component-based architecture enables future compatibility with different PDF packages, like pdfcpu, extending its potential beyond PDFs to other document formats.

components tree

Components tree, taken from Maroto’s structure.

Now that we’ve set up our Encore project and have a better understanding of Maroto, let’s build our invoice. We will create a Go file for handling the PDF generator.

package invoice

import (
	...
)

func Generate(data *Request) (Response, error) {
	m := GetMaroto()

	document, err := m.Generate()
	if err != nil {
		log.Fatal(err.Error())
	}

	response := Response{Data: document.GetBytes()}
	return response, nil
}

func GetMaroto() core.Maroto {
	cfg := config.NewBuilder().Build()

	mrt := maroto.New(cfg)
	m := maroto.NewMetricsDecorator(mrt)

	err := m.RegisterHeader(getPageHeader())
	if err != nil {
		log.Fatal(err.Error())
	}

	m.AddRows(getBilledToInfo()...)
	m.AddRows(getTransactions()...)

	err = m.RegisterFooter(getPageFooter())
	if err != nil {
		log.Fatal(err.Error())
	}

	return m
}

func getPageHeader() core.Row {
	return row.New(35).Add(
		col.New(6).Add(
			text.New("INVOICE", props.Text{
				Size:   35,
				Align:  align.Left,
				Color:  getGrayColor(),
				Style:  fontstyle.Bold,
				Family: "courier",
			}),
		),
		col.New(6).Add(
			text.New("Really Great Company", props.Text{
				Top:   4,
				Size:  12,
				Align: align.Right,
				Style: fontstyle.Bold,
			}),
			text.New("Your Business Partner", props.Text{
				Top:   10,
				Style: fontstyle.Normal,
				Size:  10,
				Align: align.Right,
			}),
		),
	)
}

...

The Generate method creates a document using Maroto. It calls GetMaroto to configure the document’s layout, including the header, billing to, transactions rows, and footer. After generating the document, it returns the document bytes if everything goes well.

The full code for these methods, and the Request and Response structs, is available in our repository: https://github.com/wawandco/invoice-generator.

Creating the API Endpoint

Once we have our invoice template created, we will create an API endpoint that will expose this functionality. Inside api/invoice.go, define a new endpoint that calls the GenerateInvoice function to create and return our PDF.

package api

import (
	...
)

//encore:api public method=POST path=/generate-invoice
func GenerateInvoice(ctx context.Context, data *models.Request) (models.Response, error) {
	response, err := invoice.Generate(data)
	if err != nil {
		return models.Response{}, err
	}

	return response, nil
}

By adding the following annotation:

//encore:api public method=POST path=/generate-invoice

We are indicating Encore that our package api is a service, and it should serve as part of our catalog of services the POST /generate-invoice path as a public API endpoint. For those interested in exploring more about defining Type-Safe APIs, further details can be found at Encore’s Type-Safe APIs documentation.

Running the API and Testing

With the API endpoint set up and PDF generator logic in place, we can now run our Encore app locally to test the service by running the following command:

$ encore run

By running this, a few things will happen: Encore will not only serve our API locally on port 4000, but it will also provide us with a Development Dashboard where will be able to see the endpoints we have created.

encore dashboard

To test our endpoint, we can make a POST request to http://localhost:4000/generate-invoice, this should return the invoice PDF in []byte. Here’s a simple curl command to test the endpoint:

$ curl -X POST http://localhost:4000/generate-invoice -d @invoice.json

invoice.json:

{
    "CustomerName": "Jane Doe",
    "CustomerPhoneNumber": "(+1) 555-123-4567",
    "CustomerAddress": "742 Evergreen Terrace, Springfield, IL 62704",
    "InvoiceNumber": "12345",
    "InvoiceDate": "October 1, 2024",
    "Transactions": [
        {
            "Name": "Blue Denim Jacket",
            "Quantity": "1",
            "UnitPrice": "$150",
            "Total": "$150"
        },
        {
            "Name": "Striped Polo Shirt",
            "Quantity": "3",
            "UnitPrice": "$80",
            "Total": "$240"
        },
        {
            "Name": "Patterned Skirt",
            "Quantity": "2",
            "UnitPrice": "$65",
            "Total": "$130"
        }
    ],
    "Subtotal": "$520",
    "TaxPercentage": "5%",
    "TaxAmount": "$26.00",
    "Total": "$546.00",
    "BankName": "First National Bank",
    "OwnerAccountName": "John Smith",
    "OwnerAccountNumber": "123-456-789",
    "PaymentDate": "October 10, 2024",
    "OwnerName": "John Smith",
    "OwnerAddress": "123 Maple St, Anytown, CA 90210"
}

Note: The logic for calculations and currency/dates formatting can be implemented in various ways. For simplicity in this example, we opted to pass calculations and formats as preformatted string values.

The curl call will give us the generated invoice in []byte locally.

{"Data": "JVBERi0xLjMKMyAwIG9iago8PC9UeXBlIC9QYWdlCi9QYXJlbnQgMSAwIFIKL1Jlc291c..."}

If we want to see what the PDF looks like, we would need to add a Go test for it. Encore uses the Go’s built-in testing framework, making it easy to write and run tests for our applications. The key difference is that, with Encore’s additional compilation step, we need to use the Encore CLI test command instead of go test to execute our tests. This command compiles our Encore app and runs the tests. Let’s create and run our test for the Generate method to see what our PDF looks like.

package invoice_test

import (
	// ...
)

func TestGeneratePDF(t *testing.T) {
	request := &model.Request{
		CustomerName:        "John Doe",
		CustomerPhoneNumber: "(+1) 555-123-4567",
		CustomerAddress:     "742 Evergreen Terrace, Springfield, IL 62704",
		InvoiceNumber:       "12345",
		InvoiceDate:         "October 1, 2024",
		Transactions: []model.Transaction{
			{Name: "Blue Denim Jacket", Quantity: "1", UnitPrice: "$150", Total: "$150"},
			{Name: "Striped Polo Shirt", Quantity: "3", UnitPrice: "$80", Total: "$240"},
			{Name: "Patterned Skirt", Quantity: "2", UnitPrice: "$65", Total: "$130"},
		},
		Subtotal:           "$520",
		TaxPercentage:      "5%",
		TaxAmount:          "$26.00",
		Total:              "$546.00",
		BankName:           "First National Bank",
		OwnerAccountName:   "John Smith",
		OwnerAccountNumber: "123-456-789-10",
		PaymentDate:        "October 10, 2024",
		OwnerName:          "John Smith",
		OwnerAddress:       "123 Maple St, Anytown, CA 90210",
	}

	response, err := invoice.Generate(request)

	assert.NoError(t, err)
	assert.NotEmpty(t, response.Data, "PDF data should not be empty")

	outputFolder := "pdf"
	outputFile := filepath.Join(outputFolder, fmt.Sprintf("invoice_%s.pdf", request.InvoiceNumber))

	// Create the "pdf" folder if it does not exist
	err = os.MkdirAll(outputFolder, os.ModePerm)
	assert.NoError(t, err)

	err = os.WriteFile(outputFile, response.Data, 0644)
	assert.NoError(t, err)
}

Now, we can execute our test by runnig:

$ encore test ./... 

Once executed, the PDF invoice will be generated and saved at invoice/pdf/invoice_12345.pdf and should look as shown below:

pdf invoice

Congratulations 🎉, our invoice generator service is complete and ready to be used, now, let’s put it out there!

Deploying Our Invoice Generator Service

One of the most powerful features of Encore is how easy it is to deploy services to the cloud. Encore simplifies deployments by integrating Git and Github and automating all the CI and CD processes. It moves away the effort of thinking in builds, testing, and other infrastructure/DevOps tasks. A git push is enough to trigger a deployment, whether we’re using Encore’s internal Git system or have integrated our repository with GitHub. The pipeline automatically builds our application, runs tests, and provisions the required cloud infrastructure. To deploy our invoice generator, let’s commit our changes and push the code by running:

$ git add -A .
$ git commit -m 'Invoice generator'
$ git push encore

In this example, encore means we are using the Encore’s internal Git system. For a push to Github, we just need to change the remote name and push to our repo:

$ git push origin

When using GitHub, Encore offers additional features like automatic Preview Environments. When we push changes to a pull request, Encore creates an isolated environment that mirrors production. This allows us to thoroughly test and validate features in a real-world environment before merging the code into the main branch.

Environments

Encore simplifies environments creation through its cloud dashboard, whether for local, preview, staging and production, Encore isolates each of them in separated instances.

To create an environment, we just need to open our app in the Cloud Dashboard and go to the Environments page, then click on Create Environment in the top right. That will open a form page where we will need to choose a name for our environment and specify whether it will be used for production or development. We can also configure deployment triggers to either push changes from a Git branch or deploy manually. Additionally, we can choose our preferred cloud provider—such as Google Cloud, AWS or the Encore Cloud—and determine process allocation to decide whether all services should be deployed together or individually. After making these selections, click “Create” to finalize the setup.

deployed

And that’s all 🎉🎉. Our invoice generator service now has a rolled out production environment.

Conclusion

We’ve covered key aspects of our Encore-based App, focusing on its simplicity to API endpoints, deployment setup, and functionality. Additionally, the integration of Maroto has significantly facilitated our PDF invoice design. While this post wraps up the initial discussion, there’s still much more to explore with Encore. Moving forward, we will dive into additional Encore topics, enhancing our application with new features and refinements. Stay tuned for future entries where we will continue to expand on these topics and build on our existing work, diving deeper into Encore’s capabilities and integrating more advanced features into our app.

References

  1. A simplified cloud backend development workflow.
  2. Maroto V2.
  3. Encore’s installation.
  4. Maroto’s structure.
  5. Encore’s Services Catalog.
  6. Encore’s Type-Safe APIs Definition.
  7. Encore’s Preview Environments.
  8. Encore’s Cloud Dashboard.
  9. Invoice Generator Repository.