Skip to content

Utilizing API Environment Variables on Next.js Apps Deployed to AWS Amplify

Although Next.js is a Vercel product, you may choose not to deploy to Vercel due to their pricing model or concerns with vendor lock-in. Fortunately, several other platforms fully support deployment of Next.js including AWS Amplify. Whether you’re using the Next.js app directory or not, you still have API routes that get deployed as serverless functions to whatever cloud provider you choose. This is no different on AWS Amplify. However, Amplify may require an extra step for the serverless functions if you’re using environment variables. Let’s explore how AWS Amplify is deploying your API routes, and how you can properly utilize environment variables in this context.

How AWS Amplify manages Next.js API Routes

When you deploy Next.js apps via Amplify, it takes the standard build outputs, stores them in S3, and serves them from behind a Cloudfront distribution. However, when you start introducing server side rendering, Amplify utilizes Lambda Edge functions. These edge functions execute the functionality required to properly render the server rendered page. This same flow works for API routes in a Next.js app. They’re deployed to individual lambdas.

In Next.js apps, you have two (2) types of environment variables. There are the variables prefixed with NEXT_PUBLIC_ that indicate to Next.js that the variable is available on the frontend of your application and can be exposed to the general public. At build time, Amplify injects these variables, and values that are stored in the Amplify Console UI, into your frontend application. You also have other environment variables that represent secrets that should not be exposed to users. These will not be included in your build. However, neither set of these variables will be injected into your API routes.

If you need any environment variable in your API routes, you will need to explicitly inject these values into your application at build time so they can be referenced by the Next.js systems, and stored alongside your lambdas.

Injecting Environment Variables into the Amplify Build

By default, Amplify generates the following amplify.yml file that controls your application’s continuous delivery (CD). The following is that default file for Next.js applications:

version: 1
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*
      - .next/cache/**/*

To inject variables into our build, we need to write them to a .env.production file before the application build runs in the build phase. We can do that using the following bash command:

env | grep -e <VARIABLE NAME> >> .env.production

env pulls all environment variables accessible. We use the pipe operator (|) to pass the result of that command to the grep -e which searches the output for the matching pattern. In this case, that’s our environment variable which will output the line that it is on. We then use the >> operator to append to the .env.production file, or create it if it does not exist. Be careful not to use a single > operator as that will overwrite your file’s full content.

Our amplify.yml should now look like this:

version: 1
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - env | grep -e <VARIABLE NAME> >> .env.production
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*
      - .next/cache/**/*

It is important to note that you have to do this for all environment variables you wish to use in an API route whether they have the NEXT_PUBLIC_ prefix or not.

Now, you can use process.env.[VARIABLE NAME] in your API routes to access your functions without any problems. If you want to learn more about environment variables in Next.js, check out their docs.

Conclusion

In short, AWS Amplify deploys your Next.js API routes as Lambda Edge functions that can’t access your console set environment variables by default. As a result, you’ll need to use the method described above to get environment variables in your function as needed. If you want to get started with Next.js on Amplify today, check out our starter.dev kit to get started, and deploy it to your AWS Amplify account. It’ll auto-connect to your git repository and auto-deploy on push, and collaborating with others won’t cost you extra per seat.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Introducing the New Shopify and Next.js 13 Starter Kit cover image

Introducing the New Shopify and Next.js 13 Starter Kit

Intro We are delighted to announce our brand new Shopify and Next.js 13 starter kit to our Starter.dev kits collection. This Starter kit is amazing to help you and your team quickly bootstrap a custom Shopify storefronts powered by Next.js 13 App Router since it has almost every feature you need for production set up and running! Why did we build it? At This Dot Labs, we work with a variety of clients, from big corporations to startups. Recently, we realized that we have a lot of clients who use Shopify that want to build custom stores using Next.js to benefit from its features. Additionally, we know that Hydrogen Shopify has only been limited to Remix since they acquired it. We had a lot of a hard time refilling the gap Hydrogen left to implement its features from scratch in our client projects. And from here, we realized that we need to build a starter kit to: - Help our teams build custom storefronts using Next.js Quickly - Reduce the cost for our clients There were a lot of Next.js Shopify starters out there, but they were very basic and didn’t have all the features our teams needed, unlike the Official Hydrogen Starter Example which is complete. So, we decided to convert the Hydrogen starter from Remix/Hydrogen to Next.js, and use the power of the App Router and React Server Components. Tech Stack We used in this project: - Next.js 13 - TailwindCSS - Zustand Why Shopify Shopify is a great e-commerce platform. They have a lot of experience in this field since 2006, and now over 4.12 million e-commerce sites are built with Shopify. (Source: Builtwith.com.) Shopify is used by online sellers in over 175 countries around the world, with 63% of Shopify stores estimated to be based in the US. Why Next.js 13 As we mentioned above, we have a lot of clients who already use Next.js and it’s a very popular framework in the market for its flexibility, scalability, and diverse collection of features. Features Since this starter kit is based on the official Shopify Hydrogen template, it has all of it’s features as well including: - Light/Dark themes support - Authentication system with login, register, forgot password, and account pages - Supports both Mobile/Desktop screens - Has built-in infinity scroll pagination, built with Server Components - State management using Zustand - Variety of custom fonts - All components have been built with Tailwind - Static analysis testing tools pre-configured like TypeScript, Eslint, Prettier, and a lot of useful extensions - Storybooks for the consistency of the system design - GitHub Pull requests and release templates - Shopify analytics integrated with the kit - Great performance and SEO scores - And finally, incompatible performance tested by both Lighthouse and PerfBuddy Performance This kit is running at top performance! It has amazing performance, SEO, and accessibility scores. We tested it using PerfBuddy, and the results are incredible on both Desktop and Mobile. Here is the full report. > Note: PerfBuddy is an incredible free tool to analyze the performance of your web apps accurately. Check it out! Also, the results from Lighthouse are pretty fascinating: When do I use this kit? This starter kit is great for starting new custom storefronts rapidly, especially for startups to save time and money. Also, it can be used to migrate from the old Next.js storefront to the new App router directory as it has all of the examples your team will need to integrate Shopify in Next.js 13 Conclusion We are very excited to share this starter kit with you, and we hope you find it useful for your projects. We are looking forward to hearing your feedback and suggestions to improve it, and add more features to it. Also, we are planning to add more starter kits to our Starter.dev collection, so stay tuned for more updates!...

Avoiding Null and Undefined with NonNullable<T> in TypeScript cover image

Avoiding Null and Undefined with NonNullable<T> in TypeScript

Use Cases Use Case 1: Adding Two Numbers Scenario: A function that adds two numbers and returns their sum. But if one of the numbers is undefined or null, it returns the other number as-is. ` function addNumbers(a: number, b?: number | null): NonNullable { return a + (b ?? 0); } const sum1 = addNumbers(2, 3); // Returns 5 const sum2 = addNumbers(2, null); // Returns 2 const sum3 = addNumbers(2, undefined); // Returns 2 ` In this code snippet, the addNumbers()` function takes two parameters, `a` and `b`. `a` is a required parameter of type `number`, while `b` is an optional parameter of type `number` or `null`. The function uses the NonNullable&lt;T&gt; conditional type to ensure that the return value is not null or undefined. If `b` is null or undefined, the function returns the value of `a`. Otherwise, it adds `a` and `b` together and returns the sum. To handle the case where `b` is null or undefined, the code uses the nullish coalescing operator, `??`, which returns the value on its left-hand side if it is not null or undefined, and the value on its right-hand side otherwise. Use Case 2: Checking Contact Information Scenario: A class representing a person, but with optional email and phone properties. The contact()` function logs the email and phone numbers if they are defined and not null. Otherwise, it logs a message saying that no contact information is available. ` class Person { name: string; email?: string | null; phone?: string | null; constructor(name: string, email?: string | null, phone?: string | null) { this.name = name; this.email = email ?? null; this.phone = phone ?? null; } contact() { if(this.email !== undefined && this.email !== null && this.phone !== undefined && this.phone !== null) { console.log(${this.name} can be reached at ${this.email} or ${this.phone}`); } else { console.log(${this.name} has no contact information available`); } } } const john = new Person('John Doe', 'john.doe@example.com', '(123) 456-7890'); const jane = new Person('Jane Smith', null, '987-654-3210'); john.contact(); // logs 'John Doe can be reached at john.doe@example.com or (123) 456-7890' jane.contact(); // logs 'Jane Smith has no contact information available' ` Explanation: In this code snippet, the Person` class has a `name` property and optional `email` and `phone` properties. The `contact()` function checks if both `email` and `phone` are not undefined and not null before logging the details. Otherwise, it logs a message saying that no contact information is available. To initialize the properties with null, the constructor uses the nullish coalescing operator, `??`. When creating a new `Person`, you can pass null or undefined as arguments, and the class will interpret them as null. Conclusion Understanding and correctly implementing conditional types like NonNullable&lt;T&gt; in TypeScript is crucial to reduce potential code pitfalls. By reviewing examples of numerical operations and contact information handling, we've seen how this conditional type helps reinforce our code against null or undefined values. This highlights the utility of TypeScript's conditional types not only for enhancing code stability, but also for easing our coding journey. So keep experimenting and finding the best ways to deploy these tools for creating robust, secure, and efficient programs!...

Starter.dev: Bootstrap your project with zero configuration! cover image

Starter.dev: Bootstrap your project with zero configuration!

Starter.dev: Bootstrap your project with zero configuration! Table of contents - Why Starter Kits? - Why Showcases? - Getting Started - What Kits Exist Today? - Collaborate with Us - What’s Next? We’re excited to present you with starter.dev, a set of zero-configuration project kits built with your favorite tools. Each kit is configured with the following so you can focus on building features instead of spending time on configuration: - State Management - Data Fetching - Testing - Storybook - Community standardized ESLint and Prettier We’re also launching starter.dev showcases which utilizes the kits and demonstrates how to build at-scale applications and go beyond the basic TodoMVC app, though we love a good TodoMVC! Why Starter Kits? This Dot Labs engages with its clients to build modern applications, and many of them are requesting greenfield projects using the latest and greatest technologies. While starting these projects, our architects found that they were repeating a bunch of the same steps each time they needed to start a new one. Most meta frameworks ship with the bare minimum, and don’t include features like testing or Storybook out of the box, and configuring these technologies can be time consuming. With this challenge in mind, we sought to create zero-config starter_ templates to help kick _start_ projects. And thus, starter.dev was born! Why Showcases? During starter.dev’s development, Minko Gechev from the Angular team approached us about a project to help enhance Angular education tooling. You can learn more about this specific effort in the blog post announcing the GitHub Clone Showcases. Minko’s idea was to demonstrate building applications that utilize these key features of the Angular framework: Routing Forms State Management API interactions - REST or GraphQL Authentication This idea laid the groundwork for many of the starter kits we created. We’ve developed several GitHub Clone showcase to help developers understand how to best utilize the kits, and build at-scale applications that accompany our kits. Getting Started Getting started with starter.dev (pun intended) is as simple as running a scaffolding script: - Run npx @this-dot/create-starter` to run the scaffolding tool - Select one of the kits from our library from the CLI - Name your project - cd` into your project directory, install dependencies using the tool of your choice. After completing these steps, you can start building features in your new project immediately. What Kits Exist Today? This Dot is happy to ship starter.dev with the following kits: - Angular + Apollo Client + Tailwind CSS - Angular + NgRx + SCSS - Create React App + RxJS + Styled Components - Next.js + TanStack Query (formerly React Query) + Tailwind CSS - Remix + GraphQL + Tailwind CSS - Vue 3 + Apollo Client + Quasar - Qwik + GraphQL + Tailwind CSS - SvelteKit + SASS Each kit ships with the following out-of-the-box: - Testing via jest or vitest - Storybook - ESLint and Prettier Configuration - State Management - Data Fetching for either REST or GraphQL - Some starter components to demonstrate global state management and data fetching These starter kits don’t ship with E2E testing libraries, such as Cypress or Playwright, for now, because these tools come with amazing out-of-the-box configurations and do not require additional setups. However, the showcases use Cypress tests consistency which you can check out in the showcases repo. Collaborate with us Starter.dev began as an internal need, but anyone can benefit from the existence of these kits. While there is a set structure for building out new kits, This Dot welcomes requests for new kits. We’ll work with you to determine what the structure of your kit should be and then scaffold out all of the issues needed to complete the work. Our team will help build out new issues, but we encourage the community to jump in and help build as well. This is a great opportunity to collaborate with the community as both a learner and a teacher. At This Dot, two of our guiding principles are: Getting Better Together and Giving Back to the Community. We believe that starter.dev is a perfect opportunity for us to get better through collaborative efforts with community members, and helping the community through what we hope is a great open source resource. What’s Next? We want to get this tool into your hands, and improve what exists. Tell us what is working for you and not, and we’ll do our best to address those issues. Next, we want to expand our library of kits. Tell us what are some kits you would like to see. We’re looking into building out kits for SolidJS and Node backend technologies as part of our next iterations, but we’re sure there other tools people would like to see. Finally, we’ll be publishing some additional educational materials around some of the decisions and design patterns we’ve chosen for each kit and showcase. We’re excited to share our process with you....

Testing a Fastify app with the NodeJS test runner cover image

Testing a Fastify app with the NodeJS test runner

Introduction Node.js has shipped a built-in test runner for a couple of major versions. Since its release I haven’t heard much about it so I decided to try it out on a simple Fastify API server application that I was working on. It turns out, it’s pretty good! It’s also really nice to start testing a node application without dealing with the hassle of installing some additional dependencies and managing more configurations. Since it’s got my stamp of approval, why not write a post about it? In this post, we will hit the highlights of the testing API and write some basic but real-life tests for an API server. This server will be built with Fastify, a plugin-centric API framework. They have some good documentation on testing that should make this pretty easy. We’ll also add a SQL driver for the plugin we will test. Setup Let's set up our simple API server by creating a new project, adding our dependencies, and creating some files. Ensure you’re running node v20 or greater (Test runner is a stable API as of the 20 major releases) Overview `index.js` - node entry that initializes our Fastify app and listens for incoming http requests on port 3001 `app.js` - this file exports a function that creates and returns our Fastify application instance `sql-plugin.js` - a Fastify plugin that sets up and connects to a SQL driver and makes it available on our app instance Application Code A simple first test For our first test we will just test our servers index route. If you recall from the app.js` code above, our index route returns a 501 response for “not implemented”. In this test, we're using the createApp` function to create a new instance of our Fastify app, and then using the `inject` method from the Fastify API to make a request to the `/` route. We import our test utilities directly from the node. Notice we can pass async functions to our test to use async/await. Node’s assert API has been around for a long time, this is what we are using to make our test assertions. To run this test, we can use the following command: By default the Node.js test runner uses the TAP reporter. You can configure it using other reporters or even create your own custom reporters for it to use. Testing our SQL plugin Next, let's take a look at how to test our Fastify Postgres plugin. This one is a bit more involved and gives us an opportunity to use more of the test runner features. In this example, we are using a feature called Subtests. This simply means when nested tests inside of a top-level test. In our top-level test call, we get a test parameter t` that we call methods on in our nested test structure. In this example, we use `t.beforeEach` to create a new Fastify app instance for each test, and call the `test` method to register our nested tests. Along with `beforeEach` the other methods you might expect are also available: `afterEach`, `before`, `after`. Since we don’t want to connect to our Postgres database in our tests, we are using the available Mocking API to mock out the client. This was the API that I was most excited to see included in the Node Test Runner. After the basics, you almost always need to mock some functions, methods, or libraries in your tests. After trying this feature, it works easily and as expected, I was confident that I could get pretty far testing with the new Node.js core API’s. Since my plugin only uses the end method of the Postgres driver, it’s the only method I provide a mock function for. Our second test confirms that it gets called when our Fastify server is shutting down. Additional features A lot of other features that are common in other popular testing frameworks are also available. Test styles and methods Along with our basic test` based tests we used for our Fastify plugins - `test` also includes `skip`, `todo`, and `only` methods. They are for what you would expect based on the names, skipping or only running certain tests, and work-in-progress tests. If you prefer, you also have the option of using the describe` → `it` test syntax. They both come with the same methods as `test` and I think it really comes down to a matter of personal preference. Test coverage This might be the deal breaker for some since this feature is still experimental. As popular as test coverage reporting is, I expect this API to be finalized and become stable in an upcoming version. Since this isn’t something that’s being shipped for the end user though, I say go for it. What’s the worst that could happen really? Other CLI flags —watch` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--watch —test-name-pattern` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--test-name-pattern TypeScript support You can use a loader like you would for a regular node application to execute TypeScript files. Some popular examples are tsx` and `ts-node`. In practice, I found that this currently doesn’t work well since the test runner only looks for JS file types. After digging in I found that they added support to locate your test files via a glob string but it won’t be available until the next major version release. Conclusion The built-in test runner is a lot more comprehensive than I expected it to be. I was able to easily write some real-world tests for my application. If you don’t mind some of the features like coverage reporting being experimental, you can get pretty far without installing any additional dependencies. The biggest deal breaker on many projects at this point, in my opinion, is the lack of straightforward TypeScript support. This is the test command that I ended up with in my application: I’ll be honest, I stole this from a GitHub issue thread and I don’t know exactly how it works (but it does). If TypeScript is a requirement, maybe stick with Jest or Vitest for now 🙂...