I Rebuilt "Infinite Craft" with AI and Full-Stack TypeScript
Imagine you’re writing a book in English for English readers. Now, imagine being told that all the connecting words—the “ands,” “buts,” and “ors”—must be written in Latin. Sounds absurd, right? Yet, this is often what web development feels like: you write your frontend in one language, only to switch to another for the backend.
Today, we’re changing that narrative. We’re building a multiplayer version of Infinite Craft, a captivating browser game where players combine elements to create an ever-expanding universe of objects and concepts. But the game isn’t the real story here. The real story is how we’re building it—all in TypeScript, from start to finish.
We’re using Astro and React, powerful tools you might know. But we’re pairing them with something revolutionary: Freestyle. Freestyle isn’t just another framework. It’s a fundamental shift in how we approach web development. With Freestyle, you write your entire application—frontend and backend—in TypeScript. No context switching. No mental gymnastics. Just pure, consistent TypeScript throughout.
In this tutorial, you’ll handle data persistence and user interactions. You’ll create shared rooms for collaborative noun discoveries. And you’ll do it all without leaving the comfort of TypeScript. No SQL. No Python. No Java. Just the language you already know and love.
No time to code? To skip the tutorial and deploy the final game right away:
- Clone the final project from https://github.com/kevgug/multiplayer-infinite-craft.
- Follow the setup instructions below, but use this cloned repo instead of the starter project.
- Skip ahead to deploy your version to the cloud.
Let’s get the grunt work out of the way first. We’ll get our Anthropic API key for the noun-crafting magic, set up our environment, and clone the starter repository.
With an infinite gameplay, it shouldn’t come at surprise that the original Infinite Craft game uses generative AI under the hood to generate new words. In this tutorial we’ll be using one of Anthropic’s large language models (LLMs) to generate new words. Don’t worry, this won’t become a prompt engineering tutorial, since I have already written the prompt for generating words.
Note to Prompt Engineers: Feel free to use a foundation model of your choice, when we get to that stage. Just make sure you add the API key environment variable for your chosen provider - as I’ll demonstrate for Anthropic.
You’ll need to create an Anthropic account; when I signed up in March 2024, I received $5 free credit. Once you have your account, head over to the Settings tab and navigate to API keys.
Click on the Create Key button. Give your key a name (‘secret-key’ or ‘sk’ are both fine if you’re unsure). Finally, click on the second Create Key button (the one in the modal) and copy it to your clipboard. Don’t forget to copy it!
You’ll want your computer environment to remember your Anthropic API key in one source of truth, so that you can (1) use generative AI across all your locally run projects without copy/pasting around the API key and (2) without exposing your secret key in a git repository.
Open Terminal and enter the following command.
A .txt
editor will open. Insert the following line anywhere in the file, replacing the placeholder with your own Anthropic API key (starting "sk-ant"
).
Save the text file. Return to your Terminal window and enter the following command.
To verify that the environment variable has been set correctly, you can enter the following command in your Terminal:
This should display your API key. We’re all set to use Anthropic’s official NPM packages on your Mac.
Open the Start menu and search for “Environment Variables” or “Edit the system environment variables”.
Click on “Edit the system environment variables”.
In the System Properties window that opens, click on the “Environment Variables” button.
In the Environment Variables window, under the “User variables” section, click “New”.
Set the Variable name as ANTHROPIC_API_KEY
.
Set the Variable value as your Anthropic API key (starting "sk-ant"
).
Click “OK” to close each window.
Restart any open command prompt windows or applications for the changes to take effect.
To verify that the environment variable has been set correctly, you can open a new Command Prompt window and type:
This should display your API key. We’re all set to use Anthropic’s official NPM packages on your Windows machine.
We’ll be starting with a pre-built project that includes the basic setup for our multiplayer Infinite Craft game. You can clone the starter repo directly from https://github.com/kevgug/multiplayer-infinite-craft-starter.
Alternatively, navigate to the directory where you want to store the project and run one of the following commands:
Install the packages we’ll be using with the npm install
command.
To make sure everything was installed correctly, run the npx freestyle dev
command at your project path (e.g., VSCode’s integrated terminal). Then open the URL (most likely http://localhost:8910/) in your browser:
Awesome—our local development setup is ready!
You’ll notice the buttons don’t do anything yet. So let’s dive right into coding, starting with multiplayer room management.
In our multiplayer adaptation of the original game, we want players to be able to create rooms and collaborate with their friends to craft nouns in existing rooms. To enable multi-room gameplay, we’ll employ the following plan:
Room class
Purpose
Properties
Methods
Get nouns
Craft noun, given two nouns to combine
Room Manager class
Purpose
Properties
Methods
All cloud-based functionality in this project will be stored in the cloudstate folder.
Note: No need to panic. We’ll stay in TypeScript land for our entire backend, from database to RPC functionality, thanks to Freestyle. You’ll see in just a moment.
Let’s start with the room class in src/cloudstate/room.ts
. We’ll name it RoomCS
by Freestyle convention, where CS is the abbreviation of CloudState, reflecting that we’ll be running instances of this class in the cloud.
The EmojiNoun
class is a simple class that holds a noun (text
), its emoji representation (emoji
), and whether the room that crafted this noun were first to discover it (discovered
). The list of initial nouns for each room is EmojiNoun.STARTING_NOUNS
: 💧 Water, 🔥 Fire, 🌬️ Wind, and 🌍 Earth.
Importantly, the RoomCS
class is not cloud-stateful yet. To make data persist in a database, and functions like getNouns
available as an RPC (client calls method from the cloud), we simply add the @cloudstate
decorator from Freestyle and an identifier to the class.
Wasn’t that easy?
Next, let’s create the RoomManagerCS
class in src/cloudstate/room-manager.ts
. We’ll add a roomsMap
property to store all rooms, and a roomExists
method to check if a room with a given ID exists.
Again, to make the RoomManagerCS
class cloud-stateful, we simply add the @cloudstate
decorator and an identifier. In this case, there will only ever be one RoomManagerCS
instance. So, we use a static constant identifier to let Freestyle know that this class is a singleton.
Important Note: Cloudstate classes seamlessly integrate with custom classes as properties and return types, but those classes must also be cloudstate classes.
What good is a room manager if we can’t create rooms? Let’s add a createRoom
method to the RoomManagerCS
class, directly below the roomExists
method.
Important Note: To ensure a dynamically created cloudstate instance such as
RoomCS
persists, it must be stored in a data structure property of another cloudstate class. While any collection (e.g., list, set, map) works, choose the most appropriate for your use case. InRoomManagerCS
, a map is ideal for storing bothRoomCS
instances and their unique IDs.
The createRoom
method generates a new room ID, creates a new room instance, and stores it in the roomsMap
.
Now that we have the basic room management set up, let’s create a new room when the user clicks the “Create Room” button in the Room Manager screen on the frontend.
In src/components/RoomManager.tsx
, there are unimplemented createRoom
and joinRoom
methods. We’ll implement the createRoom
method first.
With a traditional backend, we’d need to send an HTTP request to the server to create a new room, using a RESTful API or GraphQL. But with Freestyle, we can call the createRoom
method directly from the frontend, as if it were a local function.
Here we’ve created an instance of the RoomManagerCS
class and call the createRoom
method on it. Since we’ve set up the RoomManagerCS
class as a singleton, we can use the RoomManagerCS.id
constant to identify the cloud instance, but 'room-manager'
would work just as well.
Note that all cloud functions are asynchronous, so we need to use async
and await
when calling them.
Once the room is created, we’ll want to navigate to the game screen for that room.
Astro’s client side navigate
method will redirect the user to the game screen for the newly created room.
You can should see that the URL changes to /room/
followed by a random UUID. This is the ID for the room you just created.
We’ll use it to join the room in the next section, so copy it to your clipboard or save it somewhere else temporarily. Mine is 7d8eac6d-b689-48ee-bc56-f66b2ff87d85
.
To join a room on the client side, we’ll need to implement the joinRoom
method in src/components/RoomManager.tsx
. Joining a room is as simple as navigating to the game screen for that room, but we need to ensure the room exists before we do so.
Since we know that an empty string will never be a valid room ID, we can add a check to the joinRoom
method to avoid unnecessarily calling the roomExists
cloudstate method.
Since we know that an empty string will never be a valid room ID, we can add a check to the joinRoom
method to avoid unnecessarily calling the roomExists
cloudstate method.
Using Freestyle, it’s easy to forget that we’re making cloud calls, because we’re (1) writing all code in TypeScript and (2) running a zero-latency local development server via
npx freestyle dev
. But remember that every method call on a cloudstate class is a network request in production - just very well abstracted away for the developer.
Since we know that an empty string will never be a valid room ID, we can add a check to the joinRoom
method to avoid unnecessarily calling the roomExists
cloudstate method.
Important Note: Freestyle’s zero-latency local development server (via
npx freestyle dev
) and TypeScript integration make cloud calls feel seamless. However, in production, each cloudstate method invocation is an actual network request. Optimize your code to minimize these calls, just as you would with any API—as we’ve done here.
To let the player know when a room doesn’t exist, we can present a clean toast notification with error styling using the sonner
NPM package, which we’ve already installed and imported.
Try joining a room that doesn’t exist. You should now see an error toast notification animate into the bottom right of the screen.
Now let’s test joining a room that we know exists. Try submitting the room ID you set aside earlier. For me, that’s 7d8eac6d-b689-48ee-bc56-f66b2ff87d85
.
You should see the URL change to /room/<your-room-id>
. If you see the (unimplemented) game screen, you’ve successfully joined the room!
Notice we didn’t need to use fetch or Axios to make HTTP requests to the server even once! Freestyle turned our RoomManagerCS
class into a cloud-stateful class, so we could call its createRoom
and roomExists
methods directly from the frontend.
Long gone are the old days of context switching between languages with an awkward REST API communication layer in between. We’re building a full-stack application in TypeScript, from the database to the frontend.
Let’s move on to the game screen, where we’ll implement the infinite crafting gameplay.
In our starter project, we’ve already set up dynamic routing to handle URLs with the pathname /room/<room-id>
. In Astro, this is done by creating a src/pages/room/[roomId].astro
file, where [roomId]
is a dynamic parameter. The roomId
parameter is conveniently available in Astro’s params
object.
Let’s take a look at the game screen in src/pages/room/[roomId].astro
. Currently, the game screen has a navigation bar at the top and footer at the bottom. What’s missing is obviously the game itself.
We will display the game in the center of the screen, below the navigation bar. The game and its logic is in a React component, which we’ll import from src/components/Game
.
The Game
component takes two props:
roomId
—the ID of the room the player is in.nouns
—the list of nouns the room has crafted so far.For now we’ll hardcode the nouns
prop to EmojiNoun.STARTING_NOUNS
.
Astro Newcomers: The
client:load
directive tells Astro, our meta-framework, to load the ReactGame
component on the client side. Without this directive, Astro by default renders UI components on the server side with just HTML and CSS, meaning no interactivity. This default behavior is great for SEO and performance, but we want our game to be interactive, so we load theGame
component with JavaScript on the client side.
Save your code and navigate to http://localhost:8910/room/any-room-id.
Note: You could replace
any-room-id
with any string you like, as long as it’s not empty, since we aren’t yet validating room IDs.
It looks like our hardcoded list of nouns is rendering correctly as chips, with some guiding text above. But we’re not able to interact with the game just yet.
Let’s connect the Game
component to the cloudstate data for the room.
All TypeScript written in .astro
files runs on the server side (once compiled to JavaScript), which is typically far more performant than running on the client side. So, our approach is to quickly retrieve the initial list of nouns from the cloudstate server on the server side, and pass it to the Game
component as a prop. That explains why we’re not just passing the roomId
prop to the Game
component, but also the nouns
prop.
We’ll get the room ID from the URL by accessing Astro.params.roomId
(Astro is not an import, but a global object provided by Astro). We’ll then use the useCloud
hook to get the RoomCS
instance and call the getNouns
method on it.
We can now pass the roomId
and roomNouns
props to the Game
component.
If you’re still at http://localhost:8910/room/any-room-id, you should see a cloudstate error meesage, because there is no instance of RoomCS
by the ID of any-room-id
.
Let’s make sure invalid room IDs are handled gracefully. We’ll check the room exists before proceeding with fetching the nouns, by calling the roomExists
method on the RoomManagerCS
instance.
Now, if a player navigates to a room ID that doesn’t exist, they’ll be redirected to the room manager screen at the index page. We’re now fully done with the initial game loading.
Let’s move on to adding cloudstate functionality in the Game
component to get the list of nouns.
In the starter code, the Game
component takes in the initial game state (the roomID
and the list of nouns
) as props. We then pass those same props to the ChipList
component, which will render the list of nouns as chips.
Unlike previous direct uses of the useCloud hook for cloudstate classes, we’ll want to use TanStack Query for managing game state.
TanStack Query Newcomers: TanStack Query is a data-fetching library for React that provides a set of hooks for fetching, caching, and updating data. Freestyle works seamlessly with TanStack Query, so you can use it to fetch data from your cloudstate classes.
To use TanStack Query, we need to first wrap the ChipList
component in a QueryClientProvider
.
Next, we’ll use the useQuery
hook to fetch the list of nouns from the cloudstate server on demand with the refetch function, which we’ll give a custom name of refetchNouns
. We’ll store the data in the nouns
variable.
The queryKey
is an array that uniquely identifies the query. In this case, we’re using the room ID and the string 'getNouns'
. The queryFn
is a function that fetches the data, in this case calling the getNouns
method on the RoomCS
instance. The initialData
is the initial data to use while the query is fetching.
If you create or join a room, you should see the same initial list of nouns as before, because the list of nouns in the RoomCS
class is initalized with EmojiNoun.STARTING_NOUNS
. Notably, data is now being fetched from the cloudstate server!
We’re now ready to implement the core gameplay logic: combining two nouns to create a new noun.
In the original Infinite Craft game, players combine two nouns to create a new noun. For example, combining “Fire” and “Water” might create “Steam”.
Here are the rules for combining nouns in our game:
Combinations are deterministic
Combinations are commutative
Two distinct pairs of nouns can result in the same output noun
Combinations can result in one of the original nouns
Nouns can combine with themselves
Before we write any crafting logic, we’ll want to set up a caching system for generated nouns, according to the above rules.
Let’s create a new cloudstate class, NounManagerCS
, in src/cloudstate/noun-manager.ts
.
The comboKeysMap
property is a map that stores the generated nouns by the key of the pair of nouns that created them. The addKeyAndNoun
method adds a new key-value pair to the map. The didTryCombo
method checks if a noun combination has been tried before. The getNoun
method retrieves the noun for a given key (with a forced non-null assertion that assumes the key exists in the map).
Note: If you look at the
EmojiNoun
class insrc/cloudstate/noun.ts
, you’ll see we’ve already written acreateKey
method to ensure consistent and commutative keying of noun pairs. We’ll use this later in the crafting logic inRoomCS
.
Unfortunately, getNoun
currently returns the same instance of the noun, which makes it vulnerable to mutation across all rooms that try the same combination and call getNoun
. To prevent this, we’ll need to clone the noun before returning it. We can do this by spreading the noun’s properties into a new object.
The NounManagerCS
class is still one final method for checking if a noun has been discovered before, regardless of the combination that created it. We’ll add a hasNoun
method that checks if a noun appears as a value in the map at least once (remember, the same noun can be generated by different combinations).
We’re almost set with the NounManagerCS
class. We just need to make it cloud-stateful as a singleton, like we did with the RoomManagerCS
class, so that all rooms can access the same noun cache.
Perfect! We’re now ready to implement the noun crafting logic in the RoomCS
class.
In the RoomCS
class, we’ll add a craftNoun
RPC method that combines two nouns to create a new noun. Inside the method, we’ll check if the combination has been tried before, and if not, we’ll generate a new noun and cache it in the NounManagerCS
instance.
Since we want to access the methods of NounManagerCS
from another cloudstate class, RoomCS
, we’ll use the useLocal
hook. This hook is similar to the useCloud
hook, but it’s used to access cloudstate instances from within other cloudstate instances.
Important Note: We’re using the
useLocal
hook instead ofuseCloud
because we’re already within a cloudstate context.useLocal
allows us to access and modify cloudstate data without making additional network requests. It also ensures that any changes we make are part of the same transaction as the parent method, maintaining data consistency.
The first condition we’ll check is if the combination has been tried before. If it has, we’ll retrieve the cached noun from the NounManagerCS
singleton, and because the noun is cached we know it’s not a discovery. If the combination hasn’t been tried before, we’ll generate a new noun, determine if it’s a discovery to all rooms, and cache it in the NounManagerCS
singleton.
When implemented, the _generateNoun
method will generate a new noun from the combination of two nouns passed as a key. The method is static because it doesn’t need access to the instance properties of RoomCS
, and the underscore prefix lets Freestyle know that the method should not be exposed as an RPC to the client.
But first, let’s finish the craftNoun
method. We’ll add the logic to update the room’s list of nouns with the new noun, only if it’s new to the room represented by the RoomCS
instance.
Finally, we’ll return the noun that was either generated or retrieved from the globally cached nouns. We’ll extend the EmojiNoun
class with an isNewToRoom
property that lets the client know, without recomputing what’s already been computed on the server, whether the noun is new to the room.
Let’s create the new EmojiNoun
response payload class, EmojiNounRes
, at the bottom of src/cloudstate/noun.ts
.
Back in the RoomCS
class, we can now return an instance of EmojiNounRes
from the craftNoun
method. We’ll type the method as returning EmojiNounRes
and set the isNewToRoom
property based on the isNewToRoom
variable.
Onto the _generateNoun
method, after a quick overview of our generative AI approach.
For crafting nouns that are not cached, we’ll use generative AI, technically known as Large Language Models (LLMs).
Prompt Engineering Newcomers: If you’re new to LLMs, you can think of them as advanced text completion models. Simply, they’re sophisticated functions that take text as input and output the predicted best completion of that text.
Imagine your friend starts “the quick brown fox jumps over the”. You would be thrown off if they continued with “moon”. It’s not impossible, just unlikely. The primary task of LLMs is to output the most probable completion of the inputted text, with variance set by a parameter called
temperature
, which gives less probable completions more of a chance as it is dialed up. If you want the LLM to be deterministically stuck with the most likely completion, “lazy dog”, you settemperature
to 0. If you want to allow less likely completions like “moon”, you need to increase the variance and thus settemperature
closer to 1.So far, we input some text, known as the
user prompt
, and the LLM best completes it. But in most cases, we don’t simply want the model to complete a sentence. Should the text completion be creative or succinct? Should it focus on a specific topic? In our game, we want the completion to be in valid JSON format with same properties each time. We put such expectations into thesystem prompt
.The process of writing a
system prompt
that best guides the LLM to output the desired completion given an inputteduser prompt
is known as prompt engineering. We can think of prompt engineering as a functionf(x) -> y
, wheref
is thesystem prompt
,x
is theuser prompt
, andy
is the generated text output (hence the term ‘generative AI’). Closer to reality, we have an LLM trying to predict the best text completion of the combinedsystem prompt
anduser prompt
text.
If you look in the prompts folder, you’ll see we’ve already set up a prompt that instructs an LLM to generate a new noun.
Note to Prompt Engineers: This prompt is by no means perfect, but in our testing it makes for good gameplay. If you’re up for a challenge, try creating a shorter prompt that achieves similar results while cutting down on input token cost (e.g., more concise instructions, fewer examples).
We’re using an effective prompt engineering technique known as few-shot prompting: we provide a small but diverse set of input-output example pairs that demonstrate the desired behavior of the LLM. Here, few-shot prompting complements the explicit instructions on how to generate new nouns.
Let’s look at the first example.
The input will be the key of the pair of nouns to combine. We’ve already written the createKey
method in the EmojiNoun
class to ensure consistent and commutative keying of noun pairs.
Remember, if nouns A + B = C, then B + A = C.
The output will be a JSON object with two properties: obvious_choice
and witty_choice
. The obvious_choice
property will contain the most logical noun to generate, while the witty_choice
property will contain a more creative noun.
In our _generateNoun
method, we’ll call Anthropic’s API to generate a new noun from the combination of two inputted nouns, using Anthropic’s official NPM package. We’re passing in the prompt we just looked at as the system prompt and the key of the pair of nouns comboKey
as the user prompt.
Then, we’ll randomly select between the obvious and witty choice, with a bias towards the obvious choice. This bias ensures that the majority of nouns discovered are purely logical, while keeping the game interesting with the occasional witty noun.
Since Anthropic’s create
function is asynchronous, we’ll need to make the _generateNoun
method asynchronous and return a Promise
of the generated noun.
As a consequence, we’ll need to make the craftNoun
method asynchronous as well, since it calls the _generateNoun
method.
And that’s it! We’ve implemented the cloudstate logic for crafting nouns using generative AI and caching. Using Freestyle, it was super easy to integrate generative AI without exposing our system prompt to the client.
Let’s make our game interactive by making calls to the craftNoun
method on the client side.
In the Game
component, we’ll use the useMutation
hook from TanStack Query to call the craftNoun
method on the RoomCS
instance. We’ll pass in the two nouns to combine as arguments to the mutation function.
We’ll also provide an onSuccess
callback that refetches the list of nouns from the cloudstate server after a successful mutation, and an onError
callback that logs any server side errors to the console.
So that the player can interact with the game, we’ll keep a list of selected nouns in the ChipList
component. Once two nouns are selected, we’ll call the craftNoun
mutation function with the two selected nouns.
Note: In React, stateful variables don’t update as soon as their setters are called. We want read the length and values of the selected nouns array directly after updating it, but cannot rely on
setSelectedIdxs
to updateselectedIdxs
synchronously. Therefore, we store the new array state in a temporarynewSelectedIdxs
variable.
Once the mutation is successful, we’ll not only refetch the list of nouns as we’ve set up, but also clear the selectedIdxs
to allow the player to select two new nouns.
Let’s test the game by selecting any two nouns and combining them.
Success! Nouns that are new to the room are correctly added to the list of nouns.
No luck? If you’re having trouble with generating nouns, make sure you’ve stored a valid Anthropic API key as an environment variable, as outlined in the Setup section.
Alternatively, you can set your API key directly in the
Anthropic
constructor in the_generateNoun
method insrc/cloudstate/room.ts
, but remember to remove it from your code before pushing to a public repository.
Here are some possible tests you can run to ensure the crafting logic is working as expected:
While the game is now fully functional with rooms that support single and multiplayer gameplay, we can polish the user experience (UX) in three notable ways.
When a player selects two nouns to combine, we should disable all chips to prevent the player from selecting new nouns.
We’ll add an isCrafting
variable to the TanStack Query useMutation
hook, which will be set to true
when the mutation is in progress. While isCrafting
is true
, we’ll (1) disable all chips and (2) display some text that says “Crafting…” below the chip list.
When a player tries to add a noun that already exists in the room, our cloudstate logic prevents the noun from being added. We should make this clear to the player by shaking the chip of the noun that already exists and turning it red.
Let’s add a shakingIdx
state variable to the Game
component to store the index of the chip that should shake.
We’ll set this variable when the mutation succeeds and returns a response payload with the isNewToRoom
property set to false
. We’ll also set a timeout to reset the shakingIdx
after 500ms.
Finally, we’ll set the isShaking
prop on the Chip
component to shake the chip when the shakingIdx
matches the current index.
Try adding the same noun twice to see the chip shake and turn red.
Note: In case you’re wondering how the
isShaking
prop is working: we simply add theshake
CSS class to the chip whenisShaking
istrue
, which triggers the shake CSS animation as defined insrc/styles/app.css
.
Currently, the room ID is far from memorable, making it difficult and uninviting for players to share their room with friends. We’ll generate a memorable room ID for each room, using the friendly-username-generator
NPM package.
Over in the RoomManagerCS
class, we’ll add a private _generateRoomId
method that generates a memorable room ID. Unlike the current crypto.randomUUID()
function, it is also far more likely that our shorter word-based room IDs are already taken, so we’ll need to check if the generated room ID is unique.
Create a new room and you should see a more memorable room ID, such as “fearless-lapwing” in the example below. That’s undeniably easier to remember and share with friends!
Currently, the list of nouns in the room is only refreshed when the player crafts a new noun. To keep the game visually in sync across all players, we’ll automatically refresh the list of nouns in the room every second.
This is every easy to implement with TanStack Query. We just need to set the refetchInterval
option in the useQuery
hook to 1000
ms (1 second).
If you open the same room in two different windows, you should see that adding a noun in one window automatically updates the list of nouns in the other window, due to the automatic refetching.
… and that’s a wrap! 🎉
You can find the completed tutorial code at https://github.com/kevgug/multiplayer-infinite-craft.
Now that we’ve built a fully functional multiplayer version of Infinite Craft, it’s time to share it with the world. We’ll deploy our game to the cloud using Freestyle, the web development platform that allows you to code your cloud fluently in TypeScript.
Before hosting our game, we need to ensure that the environment variables we’re using in our project find their way onto the server. Currently, the Anthropic API key is only stored on your local machine, which your server won’t have access to.
Importantly, developers should never write out secret keys in their codebase, because once pushed to a public git repository they’re no longer, well, secret. Instead, we use a nifty package called dotenv
to load environment variables from a .env
file, which is by default ignored by git.
Create the following .env
file in the root of your project with your Anthropic API key.
Can’t find your API key? On macOS, open your terminal and run
echo $ANTHROPIC_API_KEY
. On Windows, open your command prompt and runecho %ANTHROPIC_API_KEY%
.
Finally, in your freestyle.config.ts
file, import the dotenv
package and call config
to load the environment variables from the .env
file. This way, the environment variables will be available on the server.
With the environment variables set up, we’re ready to host our project. The Freestyle CLI makes it easy to deploy your project to the cloud with just a few commands.
Note: Freestyle’s CLI will let you decide your own subdomain for the freestyle.dev domain, where your game will be hosted.
Easy as that—your game is now live! 🚀
We’ve hosted our own version of the game at infinitecraft.freestyle.dev. Try getting your friends to join the same room and play the game together, in either our or your own hosted version!
You’ve just created a multiplayer version of Infinite Craft, and more importantly, experienced a new approach to web development that doesn’t force you to be multilingual in your own project.
With Astro, React, and Freestyle, you’ve accomplished three key things:
This project demonstrates the power of staying in one language throughout the development process. You’ve seen how quickly ideas can become reality when you’re not constantly context-switching between languages.
You would push back against a publisher demanding you write half of your book in Latin. Now we can do the same in software development using Freestyle. The future of web development is here, and it speaks fluent TypeScript, from frontend to backend and everything in between.
Ad astra! We’re excited to see what you’ll build next, now that you can express your entire web application in TypeScript.
Credits: Thanks to Neal Agarwal for creating the original game and inspiring this tutorial.
Subscribe to the latest product updates, articles, and tutorials from Freestyle, a YC-backed company. Be among the first to build full-stack in just TypeScript.