This article will show you how to create a server using the bun Elysia Framework. By the end you will know about the following features:
Understand the bun Javascript Runtime to create an api
Basic setup and server scaffolding
Get requests with URL parameters
Post requests with body data
Architecture for organising your routes
Sending responses to the end user
I won’t cover anything generic such as database connections so that we can concentrate on the topic at hand. That said, it’s important to remember that bun is a drop in replacement for node js, so any business logic you need can simply be copy pasted from a node implementation with zero compatibility issues!
If you want even more bun goodness then check out my bun course
Setup bun Elysia App
The first step is to setup bun and create the Elysia app.
Open your working folder with “File => Open Folder”
Open a terminal with “Terminal => New Terminal”
In terminal type: “bun create Elysia myApp” (myApp is the name of your app)
Follow the onscreen instructions, simply accepting the defaults
Run Server
Now that your Elysia server has been created you should test it out. By default an npm script to run the server should be in package.json under the scripts section:
“scripts”: { “test”: “echo \”Error: no test specified\” && exit 1″, “dev”: “bun run –watch src/index.ts” },
If this is not there then go ahead and add it. Then in your terminal, build and create the Elysia instance:
npm run dev
You should see “Elysia is running at localhost:3000” in the terminal. Use your browser to go to localhost:3000 and you should see a welcome message corresponding to what’s in your index.ts server declaration code. At this point you may also want to check out my guide on hot reloading natively.
Create a Get Route
Before we start, a word on architecture … one option to create your get route is just to place it straight into the index.ts file but after adding a couple of routes that would really clutter up the index file!
Let’s use good architecture practice instead:
Create a “routes” folder in the “src” folder.
Create a “shop.ts” file in that routes folder.
This shop.ts file will hold all the routes corresponding to our pseudo online shopping server. Open that file and paste in the following code:
import{Elysia}from"elysia";constshopRoutes=newElysia({prefix:'/shop'}).get('/',()=>'HOME PAGE for shop, welcome')exportdefaultshopRoutes;
A couple of notes on the above code:
We first import the Elysia class as it contains everything we need to define all things “server”. Then we define a constant called “shopRoutes” which will hold all of our routes.
This takes an optional object in which we can define things such as the URL prefix. In this case we have defined “/shop” so this route group will only be called if the URL entered has this form:
Eg: localhost:3000/shop
You should have noticed that the server auto reloads when you save any code. The –watch option in the dev script watches all our code, automatically reloading when required!
Although if we tried this route now it wouldn’t work as we need to specify that these routes exist in the index.ts file.
Add Route Groups
Open up the index.ts file and clear out all the boilerplate code. Paste in the following lines:
import{Elysia}from"elysia";importshopRoutesfrom"./routes/shop"constapp=newElysia()app.group('',(app)=>app.use(shopRoutes)) //ADDITION OF ROUTE GROUPS.listen(process.env.PORT||3000);console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);
Our “shopRoutes” are imported at the top of file which can then be used the .group function. This registered those routes with the main Elysia object.
Now open up any browser and navigate to localhost:3000/shop to see your bun GET request in action!
Add URL Parameters to a GET Route
On a bun server you will need a way to retrieve data based on some variable passed in from the client.
For example, in our shop we may want to retrieve a product based on a code or product name. Usually this is specified in a GET route using a URL parameter, ie a subsection of the URL. The example below will take the last part of the URL as a variable which is used to find product information and return it:
myshop.com/shop/butter => Should return a page for butter myshop.com/shop/ribeye => Should return a page for ribeye steak
This is pretty easy to implement in bun. Let’s open up the routes/shop.ts file and add a second GET route as marked below:
import{Elysia}from"elysia";constshopRoutes=newElysia({prefix:'/shop'}).get('/',()=>'HOME PAGE for shop, welcome').get('/:item',({ params:params})=>"Returned data for: "+params.item) //ADD THIS LINEexportdefaultshopRoutes;
Test it out by going to localhost:3000/shop/shampoo. You should get “Returned data for shampoo” in your browser!
Let’s now step through this GET request so you understand it fully. The URL is defined as having a variable with the colon character ( : ).
Any name after that will be the variable name but you have to explicitly pass the params to the response function! This is done with the { params: params } argument, then the anonymous function will have access to your variables.
Note that the above is a shortened version of reality. You would normally be doing a database lookup somewhere based on the variable (usually an id that is passed over).
Create a POST Route with Body Data
As you probably know POST routes differ from GET routes as the former sends structured data along with the request. Usually this data would be too complex to include in a bun GET parameter so instead is bundled into the BODY of the POST request.
Using the routes/shop.ts we already have in place we will add a POST route that allows us to send over some data about a users cart. Start by adding a route as below:
import{Elysia}from"elysia";constshopRoutes=newElysia({prefix:'/shop'}).get('/',()=>'HOME PAGE for shop, welcome').get('/:item',({ params:params})=>"Returned data for: "+params.item).post('/cart',({body})=>JSON.stringify(body)) //ADD THIS LINEexportdefaultshopRoutes;
Now there are tools in Chrome that you can use to send a POST request but for sake of speed we’ll issue a curl command (on Linux / Mac). Simply copy and paste the following into your terminal:
When we send over our CURL request when also send some body data with the type “application/json” (defined in the header -H). The body data is defined after the -d flag in the curl command.
Our bun application picks up the body, converts it to a string using JSON.stringify and sends it back to us. In reality you’d actually use this data to calculate the cart price or whatever your application needs.
import{Elysia}from"elysia";importshopRoutesfrom"./routes/shop"constapp=newElysia()app.group('',(app)=>app.use(shopRoutes)).listen(process.env.PORT||3000);console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);
And here’s the code for routes/shop.ts
import{Elysia}from"elysia";constshopRoutes=newElysia({prefix:'/shop'}).get('/',()=>'HOME PAGE for shop, welcome').get('/:item',({ params:params})=>"Returned data for: "+params.item).post('/cart',({body})=>JSON.stringify(body))exportdefaultshopRoutes;
The path module in node allows us to work with files and directories. Everything you can do with the command line (or a mouse) can be done with the path module.
This blog post aims to be the vest resource out there and includes my experience as a long time node developer. If you have some tips and tricks then please leave a comment!
It’s important to note that the path module is generally platform independent, not caring what flavour of linux, windows or Mac you’re on, although typically you’re probably running Centos / Ubuntu or some other server flavour of linux.
However, just because it’s generally platform independent doesn’t mean that you won’t have issues, especially around file access permissions! Typically the process running node also needs full access to the directories you’re working with. If you get errors then permissions is usually the culprit.
Another thing to be aware of is that some commands return default directory separators for that particular operating system. Eg double backslash (\\) in Windows or single forward slash in *nix (/). Where that’s the case you should probably use the following classes:
path.win32.[your_function] which gives you double backslash separators no matter the system OS.
path.posix.[your_function] which gives you single forward slash separators no matter the system OS.
For the remainder of this reference article we will assume you’re running on some flavour of Linux server and won’t be switching between the two. If you do have to support multiple environments then just be aware of the above.
2. Information from Directory or Path
A very common use case you’ll come across is to get certain pieces of information from a file or directory. Typically you’ll mostly want the file name. Let’s take a file location such as:
/var/www/html/data/private_diary.txt
It’s very common of the beginner to think “just split the string by the / character and take the last member of the array as the filename”. Yes, you can do this but some operating systems use backslash directory separators. That means more complex code and no guarantee your code will run on another OS.
Instead, node:path has this figured out already! You can use the following:
path.basename(path, [ext])
This method allows you to extract the filename from a path, eg:
This extension removal function is case sensitive so be very careful. Normally that’s not an issue as most extensions are lower case but could be a gotcha in your case.
Personally, if I need the extension then I tend to use a string.split by period character. The last member of the array should be the extension (or if no file extension is present then there should only be one member in array, in which case there’s nothing to do). Alternatively you can use the extname method detailed later in this article.
path.dirname(path)
This method allows you to retrieve information about the path preceding the file name. Eg:
NOTE: The final directory separator ( / ) is not included in the result!
path.extname(path)
This method retrieves the extension of the file (after a period character):
path.extname(/var/www/index.ts) => .ts
NOTE 1: This method returns the period as well! Result from this can be used as the second argument in the path.basename function to strip the filename extension.
NOTE 2: If there’s no extension then this will return an empty string, so you may want to check for that.
NOTE 3: If the file name has only a period without extension eg: “myfile.” then a single period “.” will be returned.
NOTE 4 (important): If the path starts with a period eg: “.myfile” then you will get a blank string returned (GOTCHA!).
I find you need to really validate results with this method to be sure you’re getting what you expect, especially if you have uncontrolled user inputs.
path.parse(path)
Takes a path and breaks it up into constituent components accessible as properties:
path.parse(‘/var/www/site/key.txt’) will output an object with properties:
Note that if you include a delimiter ( / ) then the function will pick that up as a preconfigured path segment. Be very careful of adding posix command strings such as “..” which means to go back one level in the path, eg:
path.join(‘/foo’, ‘bar’, ‘..’) => /foo
For the above you might expect /foo/bar but addition of the “..” command makes it go back one directory!
path.resolve([…paths])
This method provides an absolute path from the paths passed in. If the first path does not include the path delimiter ( / ) then the current working directory will be added to the start of the resulting path. Eg, if your current directory is /home/user/:
4. Bonus: Other Useful path Properties and Classes
path.sep
This property returns the current path separator depending on operating system ( \ or / )
path.win32
You can use all of the methods in this post but this class will return Windows specific versions of the results (even if you’re on another OS).
path.posix
You can use all of the methods in this post but this class will return Linux specific versions of the results (even if you’re on another OS).
5. Wrap Up
There are many useful methods in node:path but there are also many gotchas if you plan on running cross platform. If that is you then test, test, test to be sure the output you’re getting is what you need! Personally I have all my paths run through the posix class (section 4) so I have no issues with delimiters. Then all you need is to be sure of absolute directories as they are different on Windows / Nix / Other.
Finally, if you enjoy this post then one of these 2 posts might interest you:
This article aims to be the most comprehensiveguide to asynchronous vs synchronous in Javascript. This is a very easy topic to understand once you actually know it. And there’s the problem, every single programmer sucks when trying to explain it!
I will initially explain these concepts using the analogy of a shopping trip, which consists of one or more people and a long shopping list.
As I proceed from synchronous to asynchronous concepts, I will weave in the history of processor development. I feel this is important for beginners to know as it gives you the right context around why we try to async almost everything these days.
Finally, we will run through the history of sync vs async code, using mostly Javascript, but also some pseudo Javascript to make things clearer.
Note: There are many nuances on this subject, not least of which is that of CPU architecture. I won’t be touching on the latter, instead simplifying it so you can really understand sync vs async. In fact even simple processors can do clever stuff with a single core but in this article I will treat them as dumb “single task” machines.
Synchronous Programming
What is Synchronous Programming?
Synchronous programming is simple to understand as it works in a linear fashion, the same way our brains do:
Synchronous programming means that a list of computation tasks happen in the order they are defined.
If you have a list of tasks then each one gets fully completed before the next one is even started. Your program will proceed down the list of tasks in this manner, never executing anything out of order.
Synchronous Programming Explained
Let’s start this section with a simple shopping analogy whereby I have a shopping list with 2 items on it, milk and bread.
At the store I cannot get milk and bread at the same time, therefore I must fetch each item sequentially. If my shopping list were to expand to 100 items then the shopping trip would take 50 times as long (ignoring walking and paying time etc).
The basic rule is thus: As long as I’m the only person doing the shopping then each doubling of the list doubles the time required, no matter what.
In programming this is the same as having a single processor (CPU) with one core (sometimes referred to as a thread). This means there is only one worker that can execute code and only one task can be running at any given time.
This single worker concept is where the idea of synchronous code comes from. One worker with one list of tasks needs to do them in a defined order.
The above code is synchronous and runs strictly in order, each line completing before the next:
Get todays exchange rate from an external service (API), then store it in “todayPrice” variable
Get yesterdays exchange rate from another API, then store it in “yesterdayPrice” variable
Subtract one from the other and assign the answer to the variable called “deltaPrice”
Now this is a very simple piece of code, but there’s an issue you may have spotted. The services fetching prices may rely on another server across the world. A call to that server could take 2 seconds to return a result which means you wait 4 seconds in total for all results.
As the average attention span is 2 seconds, you’ve already lost your user to YouTube before you even start on the second call!
Negative Impact of Synchronous Code
Imagine if you have a financial trading program and are requesting prices multiple times a second. You could lose millions of dollars in a trade because a server somewhere took 100 milliseconds too long to respond!
Or, if creating a game that runs on a single CPU, you need to prioritise either running the game or reading assets and textures from a disk for the upcoming level. The well known solution for this is “level loading”.
Or, if you have an app that continuously fetches data from disk then it would wreck the user experience, causing stuttering and annoying pauses.
So what’s the solution for running multiple tasks at once? Let’s start by looking at the evolution of processors.
Moving on from Synchronous Programming
Decades ago most processors could generally only run one task at a time (not totally true *) so synchronous programming was a natural fit. After all, if your CPU can only run one task then you cannot have anything running in parallel.
In 2002 that changed when Intel introduced hyper-threading. Hyper threading basically allows you to run 2 processes on one CPU core. This invention allowed the average consumer PC to run multiple instructions at the same time.
Shortly after that Intel even started producing multicore processors, dramatically increasing the thread count. Now you could now run 4x, 8x etc the instructions in parallel.
* Aside: Technically, multi-threading has been around since the 1950s with IBM etc but that hardware was very expensive and specialised.
* Aside 2: CPUs can have multiple “circuits” inside of them, each handling something specialised. Video decode and encode is one example. That’s why your laptop can play a lot of YouTube on battery as the specialised circuit uses very little power. However we’re ignoring this for sake of the article.
* Will the asides ever end? : If your CPU is fast enough you may not actually even notice multiple operations on one core (if the CPU has a decent instruction set). Hence the never ending race for faster and faster processing frequencies.
More Processor Cores. More Workers.
Following Intels multicore multithread revolution we now had multiple workers to do our evil code bidding. To understand why this development was utterly amazing, this let’s go back to our shopping analogy.
Shopping Analogy: If I’m one guy in a shop with one trolley and a 100 item list, I need to fetch everything myself. But if I now have 2 people on the shopping trip I can assign one task to each person to run in parallel and, in theory, we can complete the shopping trip in half the time.
Fundamentally, this increase in cores and threads is where the concept of asynchronous programming started to appear in the mass market *. If you have multiple CPU workers dividing up the task list then you can run things a lot more efficiently.
* Async coding has actually been around for a very long time, but the average developer only really got exposed to it in the early 2000s.
Asynchronous Programming Overview
What is Asynchronous javascript?
Asynchronous simply means that a list of tasks gets split between multiple workers, each executing its list at the same time as the other workers, without blocking any of the other workers from doing their tasks.
Async Javascript Explained
With the advent of multiple cores and threads, software engineers could now sub-divide their code to be split among the workers.
Each worker would take its list of tasks and process it in parallel to the other workers. In the end all the workers would return their results to be used in the next stage of the program.
A simple example may be fetching ticker prices for stocks. Let’s say you have 100 stocks that you’re watching and each price fetch takes 100 milliseconds (which is really fast). Without async programming that would take 10 seconds on a single core CPU. Clearly no good for us advanced traders…
Now, with multi core CPUs, we can assign those tasks over 16 threads, reducing the time to about half a second. This is sometimes referred to as a microtask queue.
Please note that I am grossly over simplifying this process – a network call doesn’t actually tie up the CPU thread whilst it is running!
Importance of Asynchronous Code – Time Saving
Clearly, running tasks to fetch price in a parallel manner will save us a lot of time! Nowhere is this more important than on a server. If you have a thousand users connecting every second then you want 2 things:
As many CPU threads as possible to answer simultaneous requests immediately
Fast CPU clock speeds to give users a result before they get bored and go to YouTube
This is why you see monster multicore server CPUs selling for tens of thousands of dollars. For example, Intel’s newest architecture will produce 288 core CPUs! This is called “vertical scaling” and allows each server to scale up to fulfil more and more requests.
However, there is a limit to vertical scaling. No matter the CPU you can’t serve millions of requests per second – that’s when you need horizontal scaling. But that’s a different kind of architecture beyond the scope of this article.
Asynchronous Javascript – How to Use It
Now that you understand sync vs async you’re in a good position to deep dive into a real understanding of async code. If you need a little more prompting then I’ve also written a very simple guide on the advantages of async vs sync.
In this section we will look at real coding techniques using mostly Javascript to show examples. However, the principles I’m about to show you apply to pretty much all modern languages and frameworks with very little variation.
As with the preceding text we’ll move forward in an historical manner, going over async techniques from previous years. These older techniques will provide you the necessary context so you can fully understand modern coding paradigms like async / await.
Async – Which Worker Organises the Workers?
One thing to cover before we go on is who controls the worker threads? This is generally another CPU thread (sometimes called the main thread – or controller / dispatcher). This thread is from where the tasks are sent to other workers.
The main thread as where you manage your program flow but do none of the heavy lifting.
Generally speaking, different threads of work cannot communicate with one another as each is its own universe. So, if something is calculated by one thread then it cannot share that result to another worker thread. Instead it uses something called a callback, which communicates back to the main thread.
Callbacks – Asynchronous Programming
So, we now have multiple workers doing multiple tasks in parallel, orchestrated by a main thread. The question is – how do we know when a worker finishes a task and how do we get hold of the result?
One of the first answers to this problem was the “callback”. Simply put – when the worker thread finishes its task it calls back to the main worker task with the result.
A callback is usually defined by passing in a function to the arguments when you start the long running task. When the main thread receives a callback it executes the function you passed in.
Callback Function – Code Example
Let’s now convert our synchronous code from earlier into callback based code. Read the below code very carefully including the comments (lines marked //) to understand what’s happening.
lettodayPrice=null//the following function executes WITHOUT waiting for a result. The computer simply sets the task going and IMMEDIATELY moves on to the next lines of code. The section between the curly braces {} is a function that's passed in. It's only executed when getTodaysExchangeRate calls back with a price.//This type of function is known as an "anonymous function". In Javascript it is generally defined in the format:// (callbackData) => { code to execute }getTodaysExchangeRate((price)=>{this.todayPrice=price})letyesterdayPrice=null//the following function also executes without waiting for a result. The computer simply sets the task going and IMMEDIATELY moves on to the next line of codegetYesterdaysExchangeRate((price)=>{this.yesterdayPrice=price})//The following code checks every 50 milliseconds to see // if the results are in yet from the callbacks.//FYI: "setInterval" allows you to endlessly loop code every x milliseconds//When the results are in then it prints the delta price to the console//Note that this is also a callback function!setInterval((todayPrice,yesterdayPrice)=>{if (todayPrice&&yesterdayPrice) {letdeltaPrice=today-yesterdaythis.showDelta (deltaPrice)endInterval() //stops running this checking function if prices are in}},10)functionshowDelta(deltaPrice){console.log(deltaPrice) //print price to terminal line}
The above code is extremely simplified to be readable by the new programmer so isn’t technically how you’d do things. However, it demonstrates the point I’m trying to make.
The asynchronous order of operations is:
a) getTodaysExchangeRate ( ) goes off to fetch the price on the first processor thread. When it gets the result it will assign it into the “todayPrice” variable
b) The program will immediately go the next line of code without waiting for the result (as it knows that’s coming later)
c) getYesterdaysExchangeRate ( ) fetches yesterdays price on a second thread. When it gets that result it assigns it into the “yesterdayPrice” variable
d) Without waiting for any of the above to complete, a function checks for the results every 50 milliseconds (also looping asynchronously with a callback)
If both results are present then we run the deltaPrice calculation and show it in the command line.
Note: There are much better ways to check for results of multiple asynchronous operations, rather than polling them as we’re doing with setInterval. The above code just makes the concepts easy to understand.
Note 2: Yes I’ve seen polling of async results. Yes, I’ve done it myself. We were all beginners once so don’t beat yourself up if you’ve done it!
Note 3: Code will not technically happen on a new thread for each task, as that decision is taken by your operating system (generally). However, we’re treating it as though it would.
Callback Hell (Pyramid of Doom) – Avoid This
Sometimes you need the result from an async call before you can proceed with the next part of your code.
You would come across this, for example, if you want to make sequential server calls after a user has logged in to your app. You’d perform a series of data fetches for population of app content.
In the below code we’re going to call up 3 async operations that need to be executed in order. You may wonder why we don’t just use synchronous code for this…but remember that would impact the main thread… hence, pretty much ALL of your sequential long running tasks should be offloaded to other threads.
In the below example, after each async function runs, we use the callback function to trigger the next async function. Therefore we have a stack of 3 operations which results in nested callbacks (aka: pyramid of doom):
Note that in this example we’re just faking the call time with setTimeouts lasting 1 second:
// Example of callback hell//START - 3 functions that simulate a long running callfunctionasyncOperation1(callback){setTimeout(function(){console.log("Async Operation 1 completed");callback(null,"Result of Async Operation 1");},1000);}functionasyncOperation2(callback){setTimeout(function(){console.log("Async Operation 2 completed");callback(null,"Result of Async Operation 2");},1000);}functionasyncOperation3(callback){setTimeout(function(){console.log("Async Operation 3 completed");callback(null,"Result of Async Operation 3");},1000);}//END - 3 functions// Nested callbacks, each one depending on result of previous.#asyncOperation1(function(error1,result1){if (error1) {console.error("Error in Async Operation 1:",error1);return;}console.log(result1);asyncOperation2(function(error2,result2){if (error2) {console.error("Error in Async Operation 2:",error2);return;}console.log(result2);asyncOperation3(function(error3,result3){if (error3) {console.error("Error in Async Operation 3:",error3);return;}console.log(result3);// More nested callbacks could be added, leading to further indentation and complexity});});});
The above code is horrendously hard to read with just 3 callbacks! Imagine if we had 10 of them! Therefore we need a better way to do this. Enter promises ->>>>
Avoid Callback Hell with Promises
A promise is not much different from a callback (it essentially is one) but they are much more manageable. In the previous callback examples we polled results every 50 milliseconds or had nested callbacks but clearly those are not optimal solutions.
With promises we can leave the polling and notifications to be managed by the promise module itself.
Promise definition: A promise is a function that promises to return something in the future.
Promises are such a good fit with the async paradigm that there are promise type libraries available for most languages. For example, in Javascript we would use the natively included promises module.
Let’s now see how they work with some sample code that does the following:
Fakes a long running task
Creates 5 of those tasks at the same time
Uses Promise.all() to callback when all results have been fetched
ONLY prints the results if they are all successful.
// Function representing a long running task that returns a promisefunctionasyncTask(id){returnnewPromise((resolve,reject)=>{// Simulating asynchronous operationsetTimeout(()=>{console.log(`Task ${id} completed`);resolve(`Result of Task ${id}`);},Math.random() *2000);});}// Creating an array of tasks that will run simultaneouslyconsttasks= [asyncTask(1),asyncTask(2),asyncTask(3),asyncTask(4),asyncTask(5)];// Using Promise.all() to start and execute tasks in parallel //. and ONLY call back when they are ALL completePromise.all(tasks).then(results=>{console.log("All tasks completed successfully");console.log("Results:",results);}).catch(error=>{console.error("One or more tasks failed:",error);});
Notice how there are no callback indentations littering our code. The only indentations are to catch errors thrown by a failed promise result (using then, catch). This code is also a great example of ACID principles. Using the promise results, you either pass (if all succeeded) or fail (if even just one failed).
This example would be typical if writing to a database. You would want the 5 writes toall be successfulto keep data consistent. If one write should fail then you roll back ALL the writes and retry.
Promise libraries can be architected in all kinds of different ways, for example, if you prefer to inspect or use the result from each promise callback in a chain….
Chaining Promises
To chain a list of promise tasks we use the “then” syntax, passing in a success and failure function to handle each condition. The below code chains 3 long running tasks, allowing you to inspect the result at each stage and decide whether to continue or not.
constmyPromise=newPromise((resolve,reject)=>{setTimeout(()=>{resolve("foo");},300);});myPromise.then(handleFulfilledA,handleRejectedA) //pass in 2 functions that do something according to success / error.then(handleFulfilledB,handleRejectedB).then(handleFulfilledC,handleRejectedC);
There are many, many ways to use promises depending on your needs. If you have a special case then your best option is to see the documentation for your particular promises library.
Finally, promise syntax is a lot cleaner than callbacks but there’s still a little indentation and you may need to cross scope to store results. We have finally reached what you’ve been waiting for, async and await.
Asynchronous Programming with Async & Await
No doubt you’ve heard of async / await and is what you’re here for. This is where we currently are in terms of state of the art with async programming (although it’s actually over 15 years old now!)
First, lets recap a little of what you’ve read so far, to understand where we are:
Async programming grew to take advantage of multiple CPU threads
Multiple tasks could now run in parallel
We can track multiple tasks using promises or similar paradigms but they’re not perfect
The above solutions solved some issues but presented their own problems:
Code is more readable with promises but it’s still prone to nesting and a bit difficult to decipher right away, especially for other people on your team.
Storing results from the callback means you need to cross scope which can create some funky errors.
Responsiveness of your app or server can suffer when you accidentally run async operations on the main thread (rare but can happen).
Theoretical solutions to these would be to:
Have few to no indentations in our code (more readable and fewer scoping issues)
Automatically take care of moving heavy loads off the main thread (without having to think about it).
Note: Async / await didn’t appear due to responsiveness or readability issues. It was invented as part of F# and C# as their solution for callbacks / long running tasks. But I have artistic license because it’s my website 😉
How to Use async / await
Below we will fetch some data from a text file on disk to introduce async / await concepts. Read the comments in the code to see what’s going on.
// Define a function with the "async" keyword, which allows it to be "awaited"// This function returns a promise that resolves some text after 1 secondasyncfunctiongetPriceHistoryFromTextFile(){returnnewPromise((resolve,reject)=>{setTimeout(()=>resolve("Price = $4.56"),1000)});}//define a second async function that will call the above function and alert the resultasyncfunctionprintPriceHistory(){// The line below calls our long running function from above // MAGIC SECTION: The "await" keyword tells our program to suspend execution of this particular thread of code until a result is received from the callback// This leaves the main thread free!letresult=awaitgetPriceHistoryFromTextFile()alert(result) // result fetched from above line, resume execution of code}printPriceHistory()
The basic principles of async await code:
Any long running function that will complete at some unknown point in the future should be marked with the “async” keyword
When you call that function you prefix it with the verb “await”
Await tells the program to suspend code execution until the async operation returns a result
This leaves the main thread free!
As you can see this code produces no indents, no manually managed crossing of scope and is much shorter and readable.
What’s more, if you forget to use the await keyword then you will print out a Promise – which is a very easy bug to see and fix (don’t ask how I know this).
The above reasons are why async / await syntax has become the defacto standard for multi-threaded coding.
Summary
We first covered the history of CPUs all the way from single core, to multithread and multicore monsters. This development created a way to run parallel tasks in an application and gave rise to the asynchronous paradigm. This meant improved responsiveness and processing power in software.
However, the rise of asynchronous coding created several issues. How do you get a result and get notified when it’s ready? Callbacks were the first solution for this and work great, but chaining more than 2 requests creates ugly code and scoping issues.
The solution to callbacks was promises which was a syntax solution to these issues. However it still not adequately solve these issues.
Finally we looked at async await, which is just “syntactic sugar” over callbacks and promises but solve the problems defined above. Async & await prevents indentation issues and generally guides you to the right architecture by forcing you use the await keyword.
Ov real it reliably produces a fluid, main thread free user experience!
fs (file system) module is a much misunderstood part of node JS, especially the asynchronous versus the synchronous methods. This mega post will cover everything you need to know about fs and more! I include lot’s of architecture guides and potential gotchas (many of which have gotten me in the past).
fs stands for file system, so (almost) anything you can do to a file using your terminal or mouse can also be done with fs.
This article will start with basic file writes and move on to complex writes, file reads and file management. Please do read all the text between code examples as there are many many places you can be tripped up when using fs.
What can fs do?
Because fs is part of node JS on the server it has elevated privileges that allow it to access the file system. That said, if you find that certain file operations fail then please check the linux privileges of your node JS process / server app folder.
The fs module can do many things including:
Open files
Read files
Create and write to files
Update files
Delete files
Rename files
fs Callbacks, Async and General Notes
In this mammoth article we will cover the basic methods needed to read, write and manipulate files. Bear in mind that there are 2 variations of most file manipulation commands in fs, synchronous and asynchronous. Or async and sync for shorthand.
Important Developer PSA: You should always aim to use the asynchronous file manipulation methods. This is because node JS runs on a single thread, is non blocking andevent based. Ie, node expects your code to not block the main thread unnecessarily, which is very important for the responsiveness of your server.
You will very rarely, if ever, need to use synchronous methods in fs so this guide focuses exclusively on the async methods.
Asynchronous methods are further sub divided into 3 versions – callback, promise or async based. These are all essentially the same thing, just different ways of writing code. If you’re not yet familiar with synchronous vs asynchronous programming then please click this to see my most excellent guide on it.
Personally, I always default to async await code. It’s easier to read, flows better and helps you avoid nasty problems like race conditions or cross scope variable assignment, bugs from which are very hard to diagnose.
In the first section (writing basic files) I will also show you the callback version of the sample code but future sections will only use the promise / await syntax. Just know that you can always revert to the callback versions by implementing the relevant structure (but why would you??!?!)
Basic File Writes with fs
Writing Basic Files – writeFile Function
In order to write a basic file in fs we use the writeFile method, which takes 4 parameters:
writeFile (filePath, text to write, encoding (optional, defaults to utf8), return function)
NOTE: This callback version is for reference only, I strongly advise you to use the promises / await styles in the next section!
The callback below is implemented by passing in a function as the last argument of the writeFile method. That function is executed when the tasks completes:
constfs=require("fs");letname="Grant iAmDev";//option 1: without specifying the file encodingfs.writeFile("person.txt",name,(err)=>{if (err) {console.log(err);}console.log("File saved");});//option 2: explicitly specify some options such as file encodingfs.writeFile("enc-person.txt",name,{encoding:"utf8"},(err)=>{if (err) {console.log(err);}console.log("File saved");});
Gotcha 1: writeFile will replace the file if it already exists! There will be no warnings and no thrown errors to let you know!
Gotcha 2: Do not run this method on a file whilst it is being written to by another writeFile call. You should have a flag to lock the file, preventing simultaneous writes!
Developer note: If you’re storing high frequency incoming data, or large data chunks then you should not use this method. Instead opt to use the createWriteStream method described later on.
writeFile – Promise Await Version (Avoid Callbacks)
If you prefer to avoid callback hell you can instead use the promises module included in fs. Note that the import statement changes to “fs/promises”. Quite often I’ve been caught out when visual studio auto imports just “fs” and I’m left scratching my head as to why the await statement doesn’t work!
Obviously, the promise version takes one less parameter as you don’t need to specify a callback function. Also note that we always enclose the writeFile call in a try-catch statement. This is necessary as access to a file is never guaranteed so could be a significant source of app crashes. If you have server logs ensure that a crash like this always gets recorded in there.
importfsfrom'fs/promises'asyncfunctionwriteData(name){try{returnawaitfs.writeFile("person.txt",name,"utf8") //options can use the shorthand version here, just a string automatically assigns file encoding}catch (err) {console.error('Error occurred while writing file:',err)}}
In order to call the above “writeData” function you would use the following line:
awaitwriteData("Grant @ iAmDev")
This call will run asynchronously, without tying up your main thread, but will pause execution of your code until a result is received. This function call will not blockcode in other functions from running, and keeps your app / server responsive.
If there is no error thrown then you can assume that the operation was successful. HOWEVER, I find this code a bit problematic as I usually need to know explicitly if an operation failed or succeeded. The simplest version of this is to return a boolean indicating success or failure. This is the most basic version of this code and there are other things I may do, but those are beyond scope of this article:
asyncfunctionwriteBasicFilePromise(){//always return a boolean so you know if something failed or nottry{awaitfsp.writeFile("person-promise.txt",textToWrite,"utf8")returntrue}catch (err) {console.error('Error occurred while writing file:',err)returnfalse}}//call above function and look at returned "success" to determine code flowconstsuccess=awaitwriteBasicFilePromise()console.log("File write successful? : "+success)
If you’re super fussy you may want to also assert that the file now exists and its content matches what you expect but I hardly ever find this to be necessary.
writeFile in fs – Complete List of Flags & Details
The options object passed in to writeFile allows you to specify filesystem flags that change the default behaviour when opening a file. Here’s a simple example:
awaitfsp.writeFile("person-promise.txt",textToWrite,{flag:"ax"//causes a thrown error if the file already exists!})
The following flags are available for you to use – be careful to choose the right one!
'a': Open file for writing (adding to the end of existing content) + the file is created if it does not exist (default)
'ax': Like 'a' but fails if the file path already exists
'a+': Open file for reading and writing. The file is created if it does not exist.
'ax+': Like 'a+' but fails if the file path exists.
'as': Open file for writing in synchronous mode(be careful not to tie up your main thread). File is created if it does not exist.
'as+': Open file for reading and writing in synchronous mode. The file is created if it does not exist.
'r': Open file for reading. An exception is thrown if the file does not exist so you need to catch that exception.
'rs': Open file for reading in synchronous mode. An error is thrown if the file does not exist.
'r+': Open file for reading and writing. Exception is thrown if the file does not exist.
'rs+': Open file for reading and writing in synchronous mode. Instructs the operating system to bypass the local file system cache. This is useful for opening files on NFS mounts as it allows skipping the potentially stale local cache. It has a big impact on I/O performance so using this flag is not recommended unless it is really, really needed.
'w': Open file for writing. The file is created (if it does not exist) or truncated (if it exists).
'wx': Like 'w' but fails if the path exists.
'w+': Open file for reading and writing. The file is created (if it does not exist) or truncated (if it exists).
'wx+': Like 'w+' but fails if the path exists.
Add Content to File with fs (Append)
Now that we’ve covered basic file writes let’s look at how we add content to the end of a file. This is only really convenient if you want to add to the end of a file. Adding to the middle of a file is notoriously difficult and something you’d need to manage yourself.
Side note: If you ever find you need to write a line in the middle of a file then you should really revise your architecture / storage choices. Usually a structured database, of any kind, would be a much better choice.
We can add content to a file with the appendFile method:
constfs=require('node:fs/promises');asyncfunctionappendTextToFile(){try{constcontent='Extra content at end of file';awaitfs.appendFile('/Users/iamdev/data.txt',content);}catch (err) {console.log(err);}}appendTextToFile();
Note: You can also append to a file using the writeFile method and including the flag ‘a’.
Add Content to Start of a File with node fs (Prepend)
Unfortunately there is no simple way to add content to the start of a file using the standard fs module. However there are libraries that will do this for you such as prepend-file.
Another alternative is to read the content from your existing file into memory, insert your new data and write all that back to the same file. Watch out though, a large file will occupy a lot of memory and if you don’t garbage collect properly then you’ll have mega memory leaks!
Continuous File Writes in node fs
Many times your file writes will require insertion of multiple data blocks over a longer period of time. For this operation you will want to open up a stream that allows addition of data into a file over time, whilst avoiding the overhead of re-opening the same file.
This is achieved in fs with the createWriteStream asynchronous method.
createWriteStream async for Writing Large / Continuous Files in fs
This function is fairly complicated as you are moving into a level below high level writeFile. Therefore you have more control, but also more responsibility in terms of handling file closes, errors etc.
First let’s cover the basic createWriteStream method and all the arguments it can take (with explanations):
fs.createWriteStream (filePath,OPTIONS)OPTIONScanincludeanyofthe following:options<Object>//Specify file encodingencoding<string> Default:'utf8'//Should the file be autoclosed after write? If you set// to false then you must close the file yourself!autoClose<boolean> Default:true//An event telling our code if the file has been closedemitClose<boolean> Default:true//What location should this content be inserted to?// Takes an integer from 0-MAX_ALLOWED_INTstart<integer>//highWaterMark is another way of saying buffer size before// actually writing to filehighWaterMark<number> Default:16384// File descriptor is flushed prior to closing. Defaults to false.flush<boolean>
Of particular note here is the highWaterMark option, aka: buffer size. This sets the amount of data to hold in memory until flushed to file. Note that you can get amazing memory leaks in the order of gigabytes if incorrectly specified for your use case. Whenever your server is running always use something like atop or htop to monitor resource usage!
Now let’s move onto a more substantial example. In the following we are trying to architect createWriteStream in an asynchronous manner. The code below shows you the logical steps you need to do this.
constfs=require('fs');//the main fs libtry{//always wrap in try catch as file writes can be error prone, especially on linux hosts...//create the writer object we can reuseletwriter=awaitfs.createWriteStream('log-stream.txt',{encoding:'utf8',highWaterMark:16384,flags:'a',//open and append (don't overwrite file)});//use the writer objectawaitwriter.write("Log 1: what happened in last 10s");awaitwriter.write('\r');//create a new line//sometime laterawaitwriter.write("Log 2: what happened in last 10s");//finally close the file to avoid memeroy leaks and file access issues!awaitwriter.close()}catch (err) {console.error('Error occurred while writing file:',err);}
Important notes: The above function combines the creation of the write stream (createWriteStream), the write processes and the file close, to make it clear for you. In production you would put the file open, write and close in separate functions.
Then you can call each function as required by your application.
createWriteStream Events, Manual Close etc
As we’re operating at a lower level we can specify if we want file closes to be done automatically or if we should handle them. To do this we must handle these tasks to have a complete file writing system that we control all aspects of:
Open the writer with createWriteStream
Tell that writer we wish to handle file close ourselves, specifying the autoClose flag as “false”
Register our own event listeners for open, ready and close
Write content via that file writer (normally through a separately called function)
Use listeners and callbacks to monitor state of our writes and file handling
Close the writer on completion of a subset of writes / whenever is right for your application
IMPORTANT: call removeAllListeners on completion to avoid memory leaks!
constfs=require('fs');//the main fs libtry{letwriter=awaitfs.createWriteStream("log-stream.txt",{encoding:"utf8",highWaterMark:16384,autoClose:false,emitClose:true,flags:"a"})//add a listener with options such as open, close, readyawaitwriter.addListener("open",async()=>{console.log("Log file OPEN!")})awaitwriter.addListener("ready",async()=>{console.log("Log file READY!")})awaitwriter.addListener("close",(fd)=>{console.log("Log file CLOSED")})writer.write("hello world \r\n",async()=>{awaitcloseWriter(writer)})}catch (err) {console.error('Error occurred while writing file:',err)}asyncfunctioncloseWriter(writer){awaitwriter.close(async()=>{awaitwriter.removeAllListeners() // important to avoid memory leaks!})}
The example code above does things in a stepwise, logical sequence to make it clear. Production code would never do it like this, instead separating it all into functions to be called when required by your app.
Reading files with fs
How to Use readFile in node fs
As with the writeFile function we looked at earlier the readFile function is much the same. Here’s some basic async await code:
constfs=require('node:fs/promises');asyncfunctionreadAndPrintFile(){try{//print out the index.js file that contains your server code!constdata=awaitfs.readFile('index.js',{encoding:'utf8'});console.log(data);}catch (err) {console.log(err);}}awaitreadAndPrintFile();
The above code will print out your index.js file, which should be your server entry point. CHange to any file in your directory as needed.
Note: This will read the whole file into memory. If you don’t have enough memory then your app will slow down at best, crash at worst! Obviously only use this for small files!
A better alternative is to use a stream to read files that are larger.
How to Use createReadStream Async to Read a File
A stream in Node creates a pipeline between content and your application. This avoids having to read a whole file into memory and you can instead listen out for chunks coming from your file:
asyncfunctionstreamTextFromFile(){//NOTE: This function combines createReadStream AND reads the file.// in reality you should separate these 2, only creating the stream once!try{letreader=awaitfs.createReadStream("index.js",'utf8')reader.on('error',function(error){console.log(`error: ${error.message}`);})reader.on('data',(chunk)=>{//NOTE: this outputs RAW hex data if you DON'T set the encoding above!console.log(chunk);})}catch (err) {console.error('Error occurred while stream reading file:',err)returnfalse}}awaitstreamTextFromFile()
If you don’t specify file encoding then you will receive RAW file output (in hex).
Finally
If you enjoyed this but still need help with your application or server then feel free to contact me or find me working as a software developer in Gibraltar.
node-cron is a Node.js library that allows you to schedule periodic tasks using cron syntax. It allows you to define recurring schedules by specifying minute, hour, day of the month, month, and day of the week using cron-like strings.
It’s very easy to use and you can schedule functions or tasks to execute at specific intervals, such as every minute, hourly, or on a custom schedule, including the ability to specify timezones.
Overall, node-cron simplifies the implementation of scheduled tasks in Node.js, making it a very good choice for automatic recurring processes.
How to Install node-cron
Install node-cron using the following command in your project terminal:
npminstall--savenode-cron
There are no other dependencies or tasks to complete after this step.
Import / Require node-cron
node-cron supports both commonJS and ES Modules imports. By the way, if you’re struggling with a mixture of commonJS and ES modules then I can recommend bun js as a drop in replacement to nodejs. Bun lets you mix and match commonJS and ES modules.
Require node-cron (commonJS)
Use the following statement at the top of your file to import node via “require”.
varcron=require('node-cron');
Import node-cron (ES Modules)
Use the following statement at the top of your file to import node-cron.
importcronfrom'node-cron';
Schedule a Basic Task in node-cron
In order to become fully proficient with node-corn you should first learn the cron syntax for time periods. Wikipedia will tell you all you need to know or have a look at the image below.
Cron Syntax
The following image is a basic run down of the cron syntax that node-cron relies on.
For example a command of * * * * * <code to execute> would execute your code every single minute. The asterisk (*) character denotes “every” so in this case “every minute of every hour of every day etc”.
A command of 1 * * * * <code> executes on the 1st minute of every hour, every day, every week etc.
Schedule a Task
To setup a task in node-cron simply use the imported object:
cron.schedule('* * * * *',()=>{console.log('running a task every single minute for eternity...');});
Configure node-cron Properties
The second argument when running the schedule method allows you set certain properties on the scheduled code. For example, below you can set the task to be registered but not actually scheduled to begin:
consttask=cron.schedule('* * * * *',()=>{console.log('this will not run until "task" is called with the start() method');},{scheduled:false});
Full List of node-cron Configuration Properties
Scheduled
scheduled:true/false
By default this is true and will register your job to start immediately.
Name your Task
name:'any-name-you-like'
The name is printed in logs which helps you identify where that particular entry came from.
node-cron can be setup to try and recover missed executions (eg, if your server restarts). I would advise caution with setting this to true. Otherwise, the default value is false.
recoverMissedExecutions:true
Schedule Multiple cron Times
node-cron allows you to chain multiple schedules to run the same job. Simply separate your cron values with a comma as below.
importcronfrom'node-cron';cron.schedule('0,15,30,45 * * * *',()=>{console.log('running every 15 minutes');});
Note that the comma separation (without spaces) applies to that particular entry in the cron specification.
Node-cron Specify a Range
To specify a range when you’d like your code to run then use the dash character:
importcronfrom'node-cron';cron.schedule('30-45 * * * *',()=>{console.log('running every minute to the 30th minute to the 45th');});
Node-cron Step Values (eg Every 2 minutes)
With the forward slash you can “divide” the range by a number. So in order to schedule something every 2 minutes at the start of the hour you could write:
importcronfrom'node-cron';cron.schedule('30-45/2 * * * *',()=>{console.log('Run a task every two minutes from the 30th minute');});
Node-cron Using Names for Months & Days
You can use month and day names instead of numbers if you prefer. Both full names and abbreviations work:
importcronfrom'node-cron';cron.schedule('* * * February,July Monday,Sunday',()=>{console.log('running on all Mondays and Sundays of February & July');});//ORcron.schedule('* * * Feb,Jul Mon,Sun',()=>{console.log('running on all Mondays and Sundays of February & July');});
Controlling node-cron, Methods
There are several handy methods that allow you to control the task you’ve setup.
Node-cron Schedule Method
We’ve covered this already. Simply sets up a task and schedules it.
cron.schedule('* * * * *',()=>{console.log('running every minute');});
Node-cron Start Method
If your task is setup and you choose the “scheduled: false” option then you’ll need to call start on the task to start the timer:
Any task can be stopped at any time. HOWEVER – exercise caution when using this programmatically. You may prematurely end a running task if you call stop. Best to thoroughly test this out and, of course, apply ACID principles so that you fail safe.
importcronfrom'node-cron';consttask=cron.schedule('* * * * *',()=>{console.log('will execute every minute, forever');});task.stop();
Validate Method
If you’d like to test the cron string before running it you can use the validate method:
Is hot reloading not working in your bun.js project? This article gives you 3 potential fixes (from a real developer).
First of all there are 2 ways you can hot reload code in your project:
bun –hot index.ts
bun –watch index.ts
The first one, –hot, watches your files for changes and only reloads the content. It DOES NOT restart the bun process. Therefore if your new code has some dependancy on the process restarting then it (probably) won’t be picked up. This bug may appear if you’re trying to read bun env files through a hot reload!
–hot reloading is great if you’re only working on content and want to keep state in your server across reloads. If you don’t need that then it’s much better to do a cold reload as below:
Fix 1: Use the –watch command instead of –hot
You’re much better off using the –watch command to “hot” reload the app on changes. This behaves more more like the traditional way of hot reloading in node.js, for example when using pm2, nodemon or similar process managers.
Note that each detected change causes the bun process to restart and server state will be lost.
Fix 2: WSL (Linux on Windows) doesn’t hot reload
This is a know bug when using bun.js on Windows Subsystem for Linux. The process somehow doesn’t pick up the changes in files on the Windows file system. See this issue on github: https://github.com/oven-sh/bun/issues/5155
Fix a) The best option is to run bun on native Linux or Mac of course but for some devs that’s not an option due to work place restrictions.
Fix b) Personally I use a Virtual Machine in Virtualbox or VMware and bun runs as expected. Again this may not be an option due to IT restrictions.
You don’t have to use the bun.js watcher tools. Traditional code watchers and reloaders work well too. In fact, if you’re migrating from node then you probably already have these services setup and don’t particularly want to change them out for the bun alternative.
constserver=Bun.serve({port:3000,asyncfetch(req){consturl=newURL(req.url);if (url.pathname==="/")returnnewResponse(Bun.file("index.html"),{headers:{"Content-Type":"text/html",},});returnnewResponse("404 Not Found",{status:404});},});console.log(`Server running at http://localhost:${server.port}`);
This will read an index.html file off disk and return a bun response to the caller. Let’s create a simple html file in the project root:
Note that the file API is asynchronous and that you need to call the file type as a function, in this case .json(). From there you can select properties from the file in the usual manner, eg:
console.log((awaitserverDeets.json()).version)
Be aware that if your file doesn’t exist you will get an error that stops your server. It also outputs that error to the endpoint being requested (including the code surrounding it!).
I’m not sure if that only happens in the development phase but I sure hope it doesn’t happen in production – otherwise that’s a security hole! Imagine if a private key is stored in code and gets output in that error log! (Don’t judge me, we all do it from time to time thinking we can ‘refactor’ later… 😉
How to Access bun env Environment Variables in Code
You can use the standard syntax:
process.env.VARNAME
Alternatively you can use the bun version
Bun.env.VARNAME
That’s all for the most commonly requested points on environment variables. There are a couple of other more advanced points that bun themselves document, which you can find here.
The FIRST thing you do with any app is to create authentication for it, after all you will have users and they will need accounts!
In this article I will show you the basic idea with a standalone one file express server. We will use several popular off the shelf libraries such as express and express-session to hold the users authentication.
As this is a demo it’s not suitable for production so please do not use in a production app! See the last paragraph for things you’d have to do to make it production ready!
Setup (if already done then skip to ‘Start’ below)
Setting up a Node.js environment on Windows is a straightforward process. Here are the steps to get you started:
Download the LTS (Long-Term Support) version and follow the installation wizard.
2. Verify Installation:
Open a command prompt or PowerShell window.
Check that Node.js and npm (Node Package Manager) are installed by running the following commands: bash node -v npm -v
These commands should print the installed Node.js and npm versions, confirming a successful installation.
3. Install a Code Editor (Optional):
You can use any code editor or Integrated Development Environment (IDE) of your choice. Popular options include Visual Studio Code, Atom, or Sublime Text.
4. Create a Simple Node.js Application:
Create a new file for your Node.js project named index.js and open it in your code editor.
Start – Express Server
Define our Dependencies
The first step is to define the libraries we need. Put this into the top of your index.js file. The comments give you a basic understanding of what each one does.
constexpress=require('express') //popular server frameworkconstsession=require('express-session') //to store the session details on the clients browser (that get sent with every request)constbodyParser=require('body-parser') //to 'decode' the contents of the requestconstbcrypt=require('bcrypt') //enables hashed password storage and comparison. NEVER store plain text passwords!constapp=express() //creates an express server objectconstport=3000;//defines the port our app will use (localhost:3000 in this case)
Now open a terminal in VS Code (or whatever you use), navigate to your project folder and install the libraries by running:
npmi
Reading the Request Body – Middleware
When registration or login requests come in their data payloads will be basic strings. Use this line to intercept them and change to objects that our app can use much more easily.
app.use(bodyParser.urlencoded({extended:true}));
Once this step is defined then you’ll be able to get the request data using “req.body” from anywhere within your app.
Setting a Session Object & Mock Database
Next, we tell our app to use a session object which defines the secret key used to encrypt user data. This is found in the “your-secret-key” in the below code. Feel free to change the key to your liking.
Note: You should never, ever store this key in your code (although I have, for simple demo purposes). In production you would probably store this key in environment variables!
Following the session setting we will store our registered user in a simple array called “users”. Again, this is just for dev / demo purposes. In production this data would be stored in a database.
//set teh session keysapp.use(session({secret:'your-secret-key',resave:true,saveUninitialized:true}));// Sample in-memory database for storing usersconstusers= [];
Your server should start and you get a message in the console telling you which port the server is listening on.
From now on you’ll need to restart your server after each code change. Do this with Control + C to stop the current server and then rerunning node index.js.
Alternatively, you can download a library that watches your files for changes and auto reloads for you (such as pm2 or nodemon). I use pm2 for my production servers but both libraries work well.
Setup an HTML Template for Registration and Login
For now our server doesn’t actually serve anything useful. Let’s set up 2 routes that return registration and login forms:
The /register endpoint is a “get” route that returns an HTML template for the user to register.
The /login endpoint is also a “get” route for that new user to login.
The following code is VERY basic HTML and consists of a simple form with email and password inputs. In production you’d never have something this basic – you’d at least want to validate the email address and force a minimum password length / complexity.
// app.get defines the 'GET' request - which is what your browser sends when you type an address into the bar. The '/register' part tells this block to deliver the HTML if the user goes to 'yoursite.com/register'.app.get('/register',(req,res)=>{// res.send responds to the request with the HTML form contentres.send(` <form method="POST" action="/register"> <label for="username">Email:</label> <input type="text" id="email" name="email"><br> <label for="password">Password:</label> <input type="password" id="password" name="password"><br> <input type="submit" value="Register"> </form>`);});app.get('/login',(req,res)=>{res.send(` <form method="POST" action="/login"> <label for="username">Email:</label> <input type="text" id="email" name="email"><br> <label for="password">Password:</label> <input type="password" id="password" name="password"><br> <input type="submit" value="Login"> </form>`);});
Registering the User and Storing Serverside
The HTML endpoints we’ve defined do nothing right now, as we need to setup code to receive and process the data.
Remember the req.body mentioned earlier? That’s what carries the users email and password when they submit the registration form.
Our server will check if that user is already registered and if not then we proceed to create a new user.
IMPORTANTNOTE: We never store passwords in plain text on a server – no matter how secure you think it is! We always ‘hash’ them (put them through a one way algorithm, using bcrypt in this case) and then store the resulting hash.
Finally we store the user in our in-memory database. In production you would use a database such as mysql, Postgres or mongo. However, databases are beyond the scope of this article.
app.post('/register',async(req,res)=>{// get submitted form data from request bodyconst{email,password}=req.body;// Check if the email is already registeredif (users.find(user=>user.email===email)) {returnres.status(400).send('Email already registered');}// Hash the password before storing itconsthashedPassword=awaitbcrypt.hash(password,10);// Store the user in the in-memory database (you might want to use a database in a real-world application)users.push({email,password:hashedPassword});console.log(users)res.send('Registration successful');});
You’re all set to navigate to /register. Go ahead and register an email and password and you should see the users array printed out to your console, complete with hashed password! For example:
Note: The password hash is ultra secure and cannot be decrypted within a billion years with all the computing power in the world. That’s the power of maths and encryption 😉
On to the next step!
Checking Login Details
Now, if you navigate to the ‘/login’ page you are presented with a form to login – which does nothing! Let’s create the backend code in the /login endpoint that allows this to actually do something.
We will use the bcrypt compare function to see if the user password matches our database hash.
app.post('/login',async(req,res)=>{const{email,password}=req.body;// Find the user by emailconstuser=users.find(user=>user.email===email);// Check if the user existsif (!user) {console.log("User doesn't exist")returnres.status(401).send('Invalid email or password');}// Compare the provided password with the hashed password in the databaseconstpasswordMatch=awaitbcrypt.compare(password,user.password);if (passwordMatch) {// Set a session variable to indicate that the user is authenticatedreq.session.authenticated=true;res.send('Login successful');}else{res.status(401).send('Invalid email or password');}});
Restart the server, register a user and navigate to /login. You should be able to enter your details and receive back a message about having had a successful login.
Middleware to Protect User Area
Now that we have login credentials we need somewhere to send the user so they can see all their private stuff. We will create a /dashboard route that will only allow access to the user if logged in.
Now, seeing as we have to check the users credentials for each protected route we should create what’s called a middleware. This is an express.js feature that executes a function between when the route is called and when it returns some data.
Middlewares are extremely useful and you will use them everywhere in express JS! Let’s create a ‘requireAuth’ middleware that will perform authentication:
// Middleware to protect routes that require authenticationconstrequireAuth=(req,res,next)=>{if (req.session.authenticated) {next();// tells express to perform the next function, which is calling the dashboard in this case}else{res.status(401).send('Unauthorized');}};
By itself this middleware does nothing and needs to be called somewhere. Create a /dashboard endpoint, noting that the second parameter is the middleware we just created.
// Example of a protected routeapp.get('/dashboard',requireAuth,(req,res)=>{res.send('Welcome to the dashboard!');});
Save your file and restart your server.
Navigate to /dashboard and you’ll see that you are unauthorised! Go through the registration -> login -> dashboard loop to see that it all works now!
Optional – Niceties
It’s a bit annoying to manually navigate to pages so let’s make a simple link page at the root of our site:
Restart server and now when you navigate to localhost:3000 you will get a page with those links.
In my code linked below I’ve also changed the registration ‘success’ message to instead tell the browser to redirect to /login. I’ve also made a redirect for the login success case which takes the user to their dashboard.
Full Code – Express JS Session Server
constexpress=require('express');constsession=require('express-session');constbodyParser=require('body-parser');constbcrypt=require('bcrypt');constapp=express();constport=3000;app.use(bodyParser.urlencoded({extended:true}));app.use(session({secret:'87fir87r8f',resave:true,saveUninitialized:true}));app.get('',(req,res)=>{res.send(` <a href="/register">Register</a><br> <a href="/login">Log In</a><br> <a href="/dashboard">Dashboard</a>`)})app.get('/register',(req,res)=>{res.send(` <form method="POST" action="/register"> <label for="username">Email:</label> <input type="text" id="email" name="email"><br> <label for="password">Password:</label> <input type="password" id="password" name="password"><br> <input type="submit" value="Register"> </form>`);});app.get('/login',(req,res)=>{res.send(` <form method="POST" action="/login"> <label for="username">Email:</label> <input type="text" id="email" name="email"><br> <label for="password">Password:</label> <input type="password" id="password" name="password"><br> <input type="submit" value="Login"> </form>`);});// Sample in-memory database for storing usersconstusers= [];app.post('/register',async(req,res)=>{const{email,password}=req.body;// Check if the email is already registeredif (users.find(user=>user.email===email)) {returnres.status(400).send('Email already registered');}// Hash the password before storing itconsthashedPassword=awaitbcrypt.hash(password,10);// Store the user in the in-memory database (you might want to use a database in a real-world application)users.push({email,password:hashedPassword});console.log(users)res.redirect('/login');});app.post('/login',async(req,res)=>{const{email,password}=req.body;// Find the user by emailconstuser=users.find(user=>user.email===email);// Check if the user existsif (!user) {console.log("User doesn't exist")returnres.status(401).send('Invalid email or password');}// Compare the provided password with the hashed password in the databaseconstpasswordMatch=awaitbcrypt.compare(password,user.password);if (passwordMatch) {// Set a session variable to indicate that the user is authenticatedreq.session.authenticated=true;res.redirect('Dashboard');}else{console.log("User password doesn't match")res.status(401).send('Invalid email or password');}});// Middleware to protect routes that require authenticationconstrequireAuth=(req,res,next)=>{console.log(req.session)if (req.session.authenticated) {next();}else{res.status(401).send('Unauthorized');}};// Example of a protected routeapp.get('/dashboard',requireAuth,(req,res)=>{res.send('Welcome to the dashboard!');});app.listen(port,()=>{console.log(`Server listening at http://localhost:${port}`);});
Finally, if you have any questions or issues then please say so in the comments!
If you use replit then you can also access this server below. Note that replit has some ‘issues’ around node js so you may see some weird redirect loop behaviour.
Creating a basic server in Express.js is very straightforward. Express.js is a minimal and flexible Node.js web application framework that provides a robust set of features to develop web and mobile applications. To create a basic server, follow these steps:
Install Node.js and npm: Ensure that you have Node.js and npm (Node Package Manager) installed on your machine. You can download them from the official website: Node.js.
Initialize Your Project: Open your terminal and create a new directory for your project. Navigate into the project directory and run the following command to initialize a new Node.js project and create a package.json file:
npminit-y
Install Express: Install Express.js as a dependency for your project. In your terminal, run:
npminstallexpress
Create Your Server Script: Create a file (e.g., app.js or server.js) where you’ll write your server code.
Write the Express Server Code: Open your server script file and write the following basic Express server code:
constexpress=require('express');constapp=express();constport=3000;// Choose a port number (e.g., 3000)// Define a routeapp.get('/',(req,res)=>{res.send('Hello, Express!');});// Start the serverapp.listen(port,()=>{console.log(`Server is running at http://localhost:${port}`);});
Run Your Server: Save your file and run your server using the following command in the terminal:
nodeapp.js
Your server will start, and you should see the message “Server is running at http://localhost:3000” (or the port you chose).
Test Your Server: Open a web browser and navigate to http://localhost:3000 (or the port you chose). You should see the message “Hello, Express!”
Congratulations! You’ve created a basic Express.js server.
This is a simple example, and Express.js provides many features for building more complex web applications, such as routing, middleware, and template engines.
As your project grows, you may want to explore these features to enhance your application.
About Express JS
Express.js is a minimal and flexible Node.js web application framework that provides a robust set of features for building web and mobile applications. It is designed to make it easier to develop server-side applications and APIs (Application Programming Interfaces) with Node.js. Here are some key aspects of Express.js:
Web Application Framework:
Express.js is a web application framework for Node.js. It simplifies the process of creating web servers and handling HTTP requests and responses.
Middleware:
Middleware functions in Express are functions that have access to the request and response objects. They can modify these objects, end the request-response cycle, or call the next middleware function in the stack. Middleware allows you to add functionality to your application’s request-response cycle.
Routing:
Express provides a simple and intuitive way to define routes for handling different HTTP methods and URIs. Routes are used to map URL patterns to functions that handle requests.
Template Engines:
While Express itself does not mandate a specific template engine, it supports integration with various template engines like EJS, Handlebars, Pug, etc. Template engines allow you to dynamically generate HTML on the server.
Static File Serving:
Express makes it easy to serve static files (like HTML, CSS, and images) using the express.static middleware.
Middleware Ecosystem:
There is a rich ecosystem of middleware available for Express.js. These middleware modules can be easily integrated into an Express application to add additional functionality, such as authentication, logging, compression, etc.
HTTP Methods:
Express supports standard HTTP methods like GET, POST, PUT, DELETE, etc. This makes it suitable for building RESTful APIs.
Community and Documentation:
Express.js has a large and active community. It is well-documented with a wealth of tutorials, guides, and examples available online.
Simplicity and Flexibility:
Express.js is designed to be minimal and unopinionated, providing developers with the flexibility to structure their applications as they see fit. It allows developers to use other libraries and tools alongside Express to meet specific project requirements.