Astro, renowned for its developer-friendly experience and focus on performance, has recently released a new version, 4.10. This version introduces two powerful features that will further streamline your development workflow and expand what you can do with Astro:
astro:env
module — A more intuitive approach to managing environment variables in Astro projectsLet’s use practical examples to learn these new features and understand how they can improve your Astro development, and by extension, web development skills.
astro:env
One may wonder, why are they improving environment variables? Why do they matter?
Well, environment variables are a key component for any website or app. They let us adjust how things work based on the environment where the project is running – be it on a local machine during development, a staging server for testing, or in production for public use.
Before the release of 4.10, developers faced several challenges when managing environment variables in Astro, some of which were the following:
The new experimental astro:env
module addresses these issues. It simplifies the management of complex environment variable configurations using a schema object defined in your Astro configuration file. Key benefits of this approach include:
With this understanding of the astro:env
module and the challenges it solves, let’s discuss how to use it in practice.
astro:env
variablesTo get started with astro:env
, first create a new Astro project by running the command below and following the prompts to create a new Astro project:
npm create astro@latest myapp
When the project is successfully created, navigate into the root folder of the project:
cd myapp
Open the Astro configuration file and update it with this snippet:
# astro.config.mjs import { defineConfig, envField } from "astro/config"; export default defineConfig({ experimental: { env: { schema: { API_URI: envField.string({ context: "server", access: "secret", default: "https://fakestoreapi.com/products", }), USER_NAME: envField.string({ context: "client", access: "public", default: "Melvin", }), }, }, }, });
In this example, we’ve created two environment variables:
API_URL
– a server-side variable (often used for secrets like API keys)USER_NAME
– a client-side variableTo use these variables in the project, open the src/pages/index.astro
file and update it with this snippet:
# src/pages/index.astro --- import { USER_NAME } from 'astro:env/client'; import {API_URI } from 'astro:env/server'; await fetch(`${API_URI}`).then(res=>res.json()).then(json=>console.log(json)) --- <html> <body> <h1>{USER_NAME}</h1> </body> </html> <style> h1 { color: orange; } </style>
In the example above:
The API_URI
environment variable is a server-side variable used to construct the URL for fetching data. Server side variables are suitable for sensitive credentials that you do not want to expose to the client, this could be API keys, secrets, authentication cookies and so on. The USER_NAME
is a client-side variable that can be accessed by client components.
This approach neatly separates client and server logic, enhances code organization, security, and maintainability.
astro:env
to control builds for different environmentsThis is another area where great environment variable management experience is beneficial to developers.
Environment variables are crucial for adapting applications to different contexts (development, staging, production) and we can leverage the new astro:env
module to further optimize this experience. Astro loads these variables in a specific order:
.env
file in the project root.env
files (e.g., .env.development
, .env.production
)Later files override values defined in earlier ones. Here’s an example configuration:
import { defineConfig, envField } from 'astro/config'; export default defineConfig({ experimental: { env: { schema: { API_URL: envField.string({ default: 'http://localhost:3000/api', // Development default context: 'server', }), NODE_ENV: envField.string({ default: 'development', context: 'client', }), }, }, }, });
In this example, the API_URL
will default to http://localhost:3000/api
if no other environment variable is set. However, if we have a .env.production
file that defines an API_URL variable, it will take precedence when deployed to a production environment:
API_URL=https://our-production-api.com
The NODE_ENV
variable is accessible on the client-side and can be used to conditionally render different UI elements based on the environment. For instance, if we have a .env.development
file in the root of our project that defines a ASTRO_APP_ENVIRONMENT
variable, we can use process.env.ASTRO_APP_ENVIRONMENT
to access its value and conditionally update the UI based on that value.
function MyComponent() { const environment = process.env.REACT_APP_ENVIRONMENT; return ( <div> {environment === "development" ? ( <div>This is the development environment</div> ) : ( <div>This is the production environment</div> )} </div> ); } export default MyComponent;
Another very important area where the astro:env
module can help improve developer experience is in CI/CD workflows. Let’s explore what is possible there:
CI=true
, DEPLOYMENT_ENV=production
)astro:env
(as of Astro 4.10)The major concern for using the astro:env
module is that it is still an experimental feature in Astro 4.10, so it might change in future releases. Keep an eye on the official documentation for updates.
The Astro Container API is the second important feature that shipped in v4.10 to allow developers to use Astro components outside of an Astro project.
The core idea of the Container API is to be able to embed your Astro components in other frameworks. This is a huge one because it opens up a whole new world of possibilities.
According to the Astro documentation, with the Astro Container API, you can leverage Astro’s strengths alongside your preferred framework’s features.
However, if you have an existing project built with another framework and would like to take advantage of this feature, you can start using Astro components gradually without rewriting your entire codebase.
The process of using the Container API involves two main steps:
astro:build
to compile Astro components into standalone, self-contained bundlesTo illustrate the Astro Container API’s capabilities, we’ll embed an Astro component within an Express application using EJS templating.
Despite the documentation suggesting broad compatibility, and my efforts trying to implement this demo in frontend frameworks like React and Vue, concrete examples and practical implementations for these frameworks remain scarce.
I believe that the fundamental differences in component lifecycle management, rendering mechanisms, build processes between Astro and these frameworks, and their inability to directly handle .astro
files directly hinder the integration.
Hence, we’ll demonstrate the Astro Container API’s functionality within an Express environment.
First Initialize a new npm project and install dependencies:
npm init -y npm install astro express ejs npm install -D nodemon
Next, create an Astro component (src/components/MyComponent.astro
):
--- --- <h1>Hello from Astro component</h1>
The above snippet will render an h1
with the text “Hello from Astro component”. Next, create a pages directory inside the src
folder and create an index.astro
file with the following snippets:
<html> <head></head> <body></body> </html>
To render or use the Astro component outside an Astro site, we first need to export it as a default export from the declaration file. Create an all.js
file inside the src
directory with the following snippet:
export { default as MyComponent } from "./components/MyComponent.astro";
The next thing we need to do is to create an Astro adapter using a JavaScript function. Astro adapters allow customization of the build process and integration with different bundlers or frameworks.
In the root directory of our project, let’s create an adapter/
folder and create an index.mjs
file inside it with the below snippet:
// Adapter/index.mjs export default function () { return { name: "myadapter", hooks: { "astro:config:done": ({ setAdapter }) => { setAdapter({ name: "myadapter", serverEntrypoint: new URL("./server-entrypoint.mjs", import.meta.url) .pathname, supportedAstroFeatures: { serverOutput: "stable", }, exports: ["manifest"], }); }, "astro:build:setup": ({ vite, target }) => { if (target === "server") { vite.build.rollupOptions.input.push("src/all.js"); } }, }, }; }
In the snippet above, we defined the adapter configuration and customized the build process with Hooks. Let’s break it down further:
name
and hooks
name
property assigns a unique name (myadapter
in this case) to our custom adapter. Astro will use this name to identify it during the build processhooks
property is where the magic happens. It’s an object containing functions that tap into specific events during Astro’s build process. These functions allow us to inject our custom logic and modify the build behavior based on the adapter’s needsNext, create the entry point file server-entrypoint.mjs
within the adapter
folder. Add the following code to it:
//adapter/server-entrypoint.mjs export function createExports(manifest) { return { manifest }; }
Here, the creatExports()
function is called by the Astro adapter to generate the final build output.
The manifest
object is essential for the adapter to understand the structure of the application and generate appropriate files or assets.
The manifest
object contains information about components, routes, and data dependencies, which is crucial for generating SSR-compatible output.
Now that we’ve defined our custom adapter, the next step is to incorporate it into the Astro build process.
To achieve this, create an astro.config.mjs
file within the project’s root directory with the following snippets:
//astro.config.mjs import { defineConfig } from "astro/config"; import adapter from "./adapter/index.mjs"; export default defineConfig({ output: "server", adapter: adapter(), integrations: [], });
Here, we imported the defineConfig
function from the astro/config
package and also imported the adapter
function from index.mjs
in the adapter
directory.
Next, we setup the defineConfig
function. The value of the output
property is set to “server
”, indicating that Astro should generate a server-side rendered application. The most crucial part is the adapter
property, which is assigned the result of calling the imported adapter
function.
The integrations property is an array that can be used to include additional plugins or integrations, but in this case, it’s left empty as we are not using any plugins.
Having established our Astro component and its dependencies, we can now proceed to the build process. Modify the script commands within the package.json
file as follows:
//package.json "scripts": { "build": "astro build", "serve": "nodemon server.mjs" }
Next, initiate the build process by running npm run build
in your terminal. Following a successful build, we will integrate the Astro component into an Express application by leveraging EJS templating.
server.mjs
)The serve
script in our package.json file
currently references a non-existent file. To address this, create server.mjs
in the root directory. This is where we’ll implement the Container API to render our Astro component within an Express environment:
//server.mjs import * as components from "./dist/server/all.mjs"; import { renderers } from "./dist/server/renderers.mjs"; import { manifest } from "./dist/server/entry.mjs"; import { experimental_AstroContainer as AstroContainer } from "astro/container"; import express from "express"; import { fileURLToPath } from "url"; import path, { dirname } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const container = await AstroContainer.create({ manifest, renderers, resolve(s) { const found = manifest.entryModules[s]; if (found) { return `/dist/client/${found}`; } return found; }, }); const app = express(); app.set("view engine", "ejs"); app.set("views", path.join(__dirname, "views")); const port = 5000; app.get("/", async (req, res) => { const html = await container.renderToString(components.MyComponent); console.log(html); res.render("index", { body: html, title: "Welcome to Astro-in-Express" }); }); app.listen(port, () => { console.log(`Server listening on port ${port}`); });
The snippet above:
dist/
) and sets up the Express app with EJSapp.get('/')
route handler to handle incoming requests to the root path. Within this handler, the container.renderToString
method is used to render the MyComponent
Astro component to a string. The rendered HTML is then passed to an EJS template named index.ejs
we are yet to create. Finally, the rendered EJS template is sent as a response to the clientNext, create a views
folder in the root directory and create an index.ejs
file with the following snippet:
<!DOCTYPE html> <html> <head> <title><%= title %></title> </head> <body class=""> <%- body %> </body> </html>
Run the npm run serve
script in the terminal to start up the server, navigate to http://localhost:5000/ and see the Astro component rendered in your Express site. There, you should now see your fully running Astro project that leverages the new astro:env
module and Container API features.
The project has better protection for sensitive credentials on the server and also leverages the Container API to run on an express Environment, which could’ve easily been a React or Vue environment. That is huge!
These new features in Astro 4.10 solve current development challenges and optimize the experiences of developers, but more importantly they hint at an exciting future for web development.
I believe the astro:env
module and Container API are only stepping stones towards a more integrated and flexible web development ecosystem. Looking ahead, we might see things like:
astro:env
evolves, it could set new standards for environment variable management across the industry and inspire other frameworks to adopt similar approachesAs web development continues to evolve, Astro is positioning itself at the forefront of innovations. These new features invite developers to experiment and reimagine what’s possible in web development.
Please try it yourself, and share your thoughts on this new direction.
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]