Loading video player...
What's up, man? Big dog. Good to see
you. My name is Josh and you and I in
this single video will build together a
secure private chat application. Now,
Josh, what is a private chat
application? Each user can create a
secure chat room to chat with other
people. They're automatically assigned a
username using a custom React hook.
We're going to write together step by
step everything. In this video, we'll go
step by step. You and I together, we
want to learn the absolutely newest
version of Nex.js. After creating a
room, you're entered into a private two
people chat. Nobody else can join your
chat. They would get an error. It's just
between you and I. And after 10 minutes,
each room is automatically destroyed and
all messages are automatically and
completely wiped from our database. Chat
apps like Signal and Telegram have had
huge success with this privacy focused
chatting. So, why don't we try it
together as well? And probably coolest
of all, messages are sent in real time.
If you type and hit enter, I see it
instantly. And the other way around,
everything in this app, sending messages
or even destroying a room whenever you
want to by clicking this button up here
destroys the room, deletes all messages,
and kicks all users from the room
instantly. Along the way, I want to
teach you so much stuff like conventions
I picked up over my jobs as a software
engineer. What's a library folder? How
do we name things? What are conventions?
Why are constants in uppercase? Best
practices and software conventions that
I learned over the years that really
helped me in my job and I want them to
help you too. We're going to learn
modern Nex.js routing patterns like
dynamic routes and how the hell they
work. We're going to learn one of the
coolest and fastest most performant
backend you can use to write Nex.js API
routes called Eliajs together. This is a
open- source project, a faster, better
way to build type- safe Nex.js backends.
It is so much nicer than writing
standard NexJS API routes. And I'm going
to teach you everything you need to
know, including middleware, type safety,
and making calls from our front end to
our back end in standard Nex.js,
deployable to Versel step by step
together, all in this single video. The
entire video is free. There's no course
I will sell you. There's there's
nothing, man. The code is open source. I
just want to teach you how I work so you
can use the same things because I
started like that 3 years ago as well.
So here's my promise to you. If you take
the 3 hours it takes to complete this
video, not even a single afternoon, you
will walk out of this video with a lot
of knowledge that you will use in your
next project and think back, hey, it was
really worth watching this video. That's
my promise. If you watch this video and
don't feel that way after the video,
dislike it, man. I dare you. And with
that said, let's start into the build.
This is a super cool project. Let's get
started step by step from the setup and
build everything together right now. All
right, man. Are you ready? We are here
inside of a browser and we're about to
learn so much stuff together. I hope
you're as hyped as I am. And let's start
very, very simply. We're going to go all
the way to the end step by step
together. Um,
you know, just so you understand
everything. I'm going to take my time,
explain every concept um so you can very
very easily follow and learn together
here with me. So let's start in the
terminal. I'm going to start right here
and I'm going to go to the location
where I want the project we're about to
create to live. So I'm going to go to my
desktop cd desktop. You could just uh
you know create a new folder on your
desktop. And here I'm going to use bun
which is a package manager. But if
you're on a Windows, even though bun is
supported there, or you're using another
package manager, any package manager
works like npx for example or yarn or
any other package manager works. I'm
going to use bunx to say create next app
at latest to initialize a new nextjs
app. Again, any package manager works
for that. Cool. Okay, that's going to
ask us what is your project named? Let's
call it real time chat. And then let's
hit enter. Perfect. Okay. Now, it's
going to ask us, would you like to use
the recommended NexJS defaults? I will
say no customized settings because
there's one option in the defaults that
I don't like, which I'm going to show
you right now. Most of the defaults are
great, right? So, we're going to say,
would you like to use TypeScript? Yes.
Which lint or would you like to use
ESLint? Great. Would you like to use
React compiler? Yes. Tailwind? Yes. And
now here's the thing. By default, the
source directory is set to no, but I
prefer it. I think it makes our entire
codebase a bit cleaner. So I will go
with a yes for the source directory and
yes for the app router. No, we don't
want to customize the default import
alias. And we're good to go. Perfect.
Now the reason I used bun to create this
and not npm is because bun is just fast.
You know, it works really well. It's
very fast package manager. That's why I
prefer it, but you can actually just use
anything. Cool. Okay. And let's cd
change directory into that folder we
just created, realtime chat. And I will
open up this project in cursor by saying
cursor dot. So basically what that means
is open the dot the current directory in
cursor. If you're following along in VS
Code, that's perfectly fine too. Great.
Let's zoom in a bunch here. Now we are
in cursor. Great. Hell yeah. All the
packages are already installed from the
installation. So what we can do is just
say bunddev or npm rundev however you
want to start your dev server and then
we can go ahead and open up zen right
here or any browser browser and go to
localhost. Here we go. This is the
standard Nex.js page that shows us hey
everything worked. We set up our project
correctly. Great man. Okay very very
cool stuff. Let's go back into cursor.
Here we go. And actually start with a
project. And we're going to start with
by far the easiest part, which is some
of the styling and layouting. So, what I
prefer for our page is to not have the
standard font right here. But actually,
let me open up the uh icons here. Here
we go. But to actually have a mono kind
of font because I think it looks a lot
nicer. So, what we're going to do is
we're going to go inside of our root
layout under source app layout, the main
NexJS layout. We're going to get rid of
the Guist fonts that they're in here by
default. And we're going to use another
font called, let me turn this
suggestions off Jet Brains Mono, one of
my all-time favorite fonts. And that
font we can actually just import
directly from next font Google because
this is a Google font called
JetBrains_mono.
Here we go. So this is nothing else in a
function. We can invoke this function
and pass it two things. A variable. For
example, let's say d- font dashjet
brains- mono, which is just a convention
to name it like this. And then the
subsets of Latin to specify which kind
of font we want to use here. Perfect.
And all we now need to do is to actually
insert this class name from Jet Brains
Mono into our body right here. So you
can see instead of the gist mono that we
already had, we can get rid of this. get
rid of the guy's mono and just insert
jet brains mono variable. Perfect. So,
if we save that, we should be able to
see this doesn't quite work yet. Why is
that, Josh? Well, let's go into our
globals CSS. Here we go. And in here, we
actually need to change one thing, and
that's going to be on the body right
here. You can see by default, NexJS uses
this kind of font family. And we just
want to change that to be the variable.
And in here, we're going to pass the
d-font
jet brains mono that we specified in our
layout. Just like this. Go ahead and
save that. And then when we go back to
our Zen browser, here we go. Perfect.
We've got a new font. And I think this
looks really, really nice. So that is
the baseline for our entire page. This
already looks really good. And what that
also means is we can actually go ahead
and get started on the actual page,
right? with the actual layouting and
design of this. So, let's start in the
root page. And I have a really cool
shortcut in my editor that lets me
select an entire div here with a single
press of a button, right? Or any like if
we are in this div right here, it will
select the entire div from start to
finish and nothing else. Now, how did I
do that? So you can also have that
because that's just you know in this
project really useful but for any
project you will ever do that's really
useful and this is under keyboard
shortcuts and then called emit emit
balance outward and I have that set to
command M right so it's emit balance
outward if you want that shortcut to set
it to any keybind that you want and
that's going to let us select an entire
div and just remove it like that like
for example the root div that's already
in this page and we're going to replace
it with a main. And to get rid of unused
imports, we could just delete this. But
a handy shortcut to do that is shift alt
and o. That's going to sort all imports
of the file if you didn't know and also
get rid of any unused ones. Just really,
really helpful for you to know. Perfect.
Let's give our main tag here a class
name of flex, a minimum height of
screen, which translates to 100 view
height, a flex call, item center justify
center, and a padding of four. Perfect.
Let's open this up. And inside of here,
we're going to put one div with a class
name of width full, a maximum width of
MD for medium, and a space y of 8.
Great. Let's open this div up. And
inside of this div, we are going to
create our main lobby form. Right? When
you join our page like this and let's
actually open this up side by side here,
just like this. Here we go. So we can
see the rendered output as well. And
let's close all the other stuff right
here. Then we're going to be able to see
everything we do live here on the right
hand side. So inside of this div, let's
open up one more. And this will get a
class name of border. a border zinc of
800, a background zinc of 90050,
and this slash50 stands for the
transparency of the background, a
padding of six, and a backdrop blur of
MD for medium to add some nice blur to
the background. Here we go. Very cool.
Let's open up one div in here. And this
will get a class name of space Y2.
Here we go. And if we open up this div,
we're going to create another div in
here. And this div is also going to get
a classim of space Y and then two. And I
actually messed up a little bit. The
first space Y here should be five. Here
we go. And one thing I will do is I will
disable my Tailwind IntelliSense plugin
because you can see every time I hover
here, it takes up the entire screen. So
it is a really useful plugin. Let me
quickly go into full screen here and
just go ahead into my extensions and
disable the Tailwind plugin because
during this video I think it's just uh
not very helpful. Here we go. And then
restart extensions. That should do the
trick. Perfect. Now it's going to be
easier for you to see what I'm doing
here. And now we can go into side by
side again. Cool. So we have one space Y
of five. And then inside of that we have
a space Y of two. Very cool. And inside
of here, let's just mock something out,
like for example, create room. We're
going to put some text here so we can
already see, hey, what we're doing here
is working. So, this already looks
pretty good, but we can make it look a
lot better. So, instead of the create
room here, we're going to create a
label. And we don't need the HTML for
here that we automatically get from the
image generation. Instead, we're going
to give it a class name. And that's
going to be flex items center and also a
text zinc of 500. And inside of here,
we're going to say your identity. So the
name the user will connect with. And
then right below the label, we're going
to create one more div with a class
name. That's going to be flex item
center and a gap of three. Let's open
this up. One more div with a class name
of flex one. A background zinc of 950.
border border zinc of 800 padding three
a text SM for small a text zinc 400 and
a font mono. Here we go. And if we open
this div up we can just render out the
username of the user. Now right now we
will get cannot find name username
because this will be dynamically
generated later. What we can do for now
is just save it in state right at the
top of our component. So we can kind of
mock it, right? So I have a shortcut for
this. It's called state and then I can
press enter and it's going to create a
use state for me here. If I go into full
screen, maybe it's a bit easier for you
to see. Basically what this shortcut
does is I can say state and it knows I
want a react use state snippet here and
then I can call it for example username
set username and then the initial value
will be just a empty string for example
right but it's the exact same as if you
were typing this out yourself. It's just
a kind of faster way to do it. Cool.
Okay. So we have the username set and
for now the error is gone. We will worry
about actually generating the username
here in just a bit. Perfect. Okay. So,
we have the username, one closing div,
two closing div, three closing divs, and
then with three more closing divs below
that, we're going to open up here and
just insert a button right here. That
button is going to get a class name of
width full background zinc 100 a text of
black padding three text SM for small a
font bold a hover BG zinc 50 a hover
text black a transition colors here we
go a margin top of two a cursor pointer
and lastly a disabled opacity 50.
Here we go. And inside of the button,
we're going to say create secure room
and hit save. Cool. Okay, we're going to
get an error as soon as we save that
saying you're importing a component that
needs use state. This react hook only
works in a client component. The easy
fix at the very top of the component
here. We need this because of the Nex.js
server component architecture. At the
very top of the file, we can just insert
use client to let the compiler know,
hey, we need React hooks inside of this
component. So, please make this a client
component and not pre-render on the
server. Right? Because you state is a
client side hook, it does not work on
the server. Cool. And now for the
username, how do we generate that? Just
to take a look what this looks like. If
we had like John, you don't need to
follow along right now. I will just show
this to you. If we had John here, we
could see John show up in the username
tab. So what we can do is just
dynamically generate this username in a
really clever way. So above our
component, let's say const generate
username is going to be just a regular
arrow function right here. And inside of
here, we will define a bunch of animals
for example, right? Or we could even do
that at the top of the function above
it. let's say const and then in all
uppercase we're going to say animals
because this is a constant. This never
changes. I was told this or taught this
at my first ever developer job where I
worked as a software engineer um for
constants. We always mark them
uppercase. So it's very easy inside of a
component if we see an uppercase word to
know, hey, this is a constant value that
never changes. So this is just a really
nice convention. And we're going to put
a bunch of animals in here like wolf,
hog. We can put bear or let's also put
shark. You can put as many as you want.
Basically, we are going to be using them
for the username generation right here.
And also what we want to do is just
quickly define a storage
key and that's going to be equal to a
chat
username. That is how we will persist
the username in local storage for this
user. So if you refresh the page, the
same username will be given to the same
user, right? And we store it under this
key right here. Beautiful. So we're
going to say const word is going to be
equal to and then the animals array
we've defined above at the index of
math. Floor and then either math do
random. Here we go. And let's uh expand
this a bit so it's easier for you to
see. And then times the animals do
length. Here we go. So basically what
we're doing here is we are accessing the
animals array at a random index. So any
one of these four animals right here.
And then we're going to return a
template string. And this allows us to
interpolate. So we can say for example
the anonymous
here we go. It's a really hard English
word, man. We're going to dynamically
interpolate the word that we randomly
selected from the array, like anonymous
wolf, for example, and then another
dash. And we want to go with a random ID
as well at the very end here. Now, ids
are kind of easy. We could go for
crypto.random UU ID, but that would be
way overkill here. So we're going to
install one package into our project
called bun install or let's say yarn add
npm install whichever package manager
you use. This package is called nano id.
I really like this package. It's good.
It's very very lightweight. And what it
allows us to do is to basically say nano
ID which we can import from nano ID and
then just invoke that with the length of
the ID we want to generate. for example,
fivedigit ID that we want to append just
to make sure this is actually a unique
username. Cool. And that's all we need
to generate username. And to make sure
one user always gets the same username
no matter how many times they refresh
the browser, we're not going to put that
in state because that would run every
time. Instead, we're going to put it
inside of a use effect right here. That
only runs when we render the page. So
we're going to leave the dependency
array at the end here empty. Inside of
this use effect, let's define a const
main that we can call right below this.
So we're going to create a function and
then call the function. So whatever
logic we now put inside the function is
going to run immediately. We're going to
say const stored. So the save username
and then local storage.get
item at the storage key. Right? So
basically we're going ahead into the
browser storage whichever browser the
user is using checking does a username
exist under this storage key called chat
username and if it does if stored is
true in that case we're going to set the
username to that stored right here.
Perfect. And we're going to return to
prevent any further code execution. But
if there is no stored username like the
user is connecting for the first time
for example then we're going to say
const generated is going to be equal to
generate username. Here we go just from
the top. So in that case we're going to
create a new username. We are going to
save that username in local storage.
We're going to say local storage set
item. Here we go. We're going to save it
under the storage
key that we already created. And we're
going to say generated right here to
store that username. Perfect. And we're
going to set the username to the
generated in that case. Perfect. Okay.
So, let's open this side by side. Here
we go. And if we refresh the browser,
we're going to be able to see cool. Our
user is now named anonymous wolf and
then a five-digit ID that we're
generating. And if we refresh this
browser, you're going to see it
persists. It stays the same. Why does it
persist? Well, because we saved it in
local storage. So, if we go to inspect
element under application, nope, not
accessibility application. Here we go.
Where is this in Firefox? Ah, I just
Googled. And in Firefox, I normally use
Chrome. It's called storage. Here we go.
So, inside of storage, we can go to
local storage and we're going to be able
to see the chat username here that's
saved. And if we were to delete this
value and reload the page, then we will
get a new ID generated for us. But this
will again be saved every time we
refresh the page. Beautiful. Okay. So
let's move this to the site. Here we go.
And the username generation logic is
entirely done. Right? That's all we need
to do for the username generation. And
just to add the final design touch to
this page, what we're going to do in the
main and then the first div with a space
Y of 8, we're going to open up here and
we're going to open up one more div with
a class name of text center and a space
Y of two. Let's open this up. And in
here we're going to put an H1, a heading
one element with a class name of text
2XL font bold tracking tight and a text
green of 500 saying inside of here
private
chat. And if we save that, we can see
bam, that's like the title of the page
here. And if we're a little bit edgy, we
can even put a like uh this kind of icon
here, the bracket. But because we can't
put uh raw brackets in here because the
syntax would be very confused. We have
to put that inside of quotes in this
curly brace. So it now looks like this.
Right? So we actually have like a kind
of terminal indication here. It looks a
bit cooler if we insert that here as a
string. And right below the H1, we're
going to create a P tag saying a private
selfdestructing
chat room. period. And that's going to
get a class name of text zing 500 and
text small. Here we go. Very, very cool
stuff. Private chat, a private
self-destructing chat room with a
automatically generated identity and a
button to create a secure room. Very,
very cool stuff, man. Now, for this
button to actually do anything, we are
going to set up two things. The first
thing we're going to set up, oh, and I
already have my browser open, is called
Elia. Now, what is Elisia? Um, here we
go. A ergonomic framework for humans.
Backend TypeScript framework with
end-to-end type safety, formidable
speed, and exceptional developer
experience. Basically, this is a really,
really good way to write very fast
Nex.js API routes natively to Nex.js JS
deployable to Verscell just a better way
to do NexJS API routes. I just did a
video about this. It's really good. This
is not sponsored in any way, by the way.
Just free open- source communitymade
software. And it's so much nicer than
standard NexJS API routes. And also,
it's a lot faster than other ways of
implementing backend routes like for
example Express. You know, Elysia can
handle 2.5 million requests per second
and Express in this benchmark 113,000,
right? It's a very very big difference.
Same for example with Hono. Hono is a
super nice type- safe backend. Elysia is
still a lot faster than Hono though. So
all in all, Elysia is a nice way to do
NexJS backends. I really enjoy it and I
want to show you how to set it up
because it's super easy and again it's
also absolutely deployable to Verscell.
So how do we set up Elia? Well, they
have a integration guide right here in
NexJS. We can just pull this up and then
oops I keep switching over here. Then we
can just have it here on the right hand
side and set it up on the left. Right.
So let's go through this together. Now
to set up Elia, the first step we're
going to do is create a folder under app
API and then we're going to call it
slugs. So let's open up our sidebar here
and under source app, we're going to
create a new folder called API. This is
just standard next, right? We would also
do the same thing in standard Nex.js,
but what's different is right here.
We're going to create a double angled
brackets dot dot dot slugs and then two
closing angled brackets. Again, this is
called a catchall route. I'm going to
get to what this does in a second. And
inside of here, we're going to create a
route.ts.
Basically, what this dot dot dot slugs
does is let me demonstrate this really
quick or visualize this really quick for
you. Let's go to Xcala draw.
Here we go. Basically what this does and
again I just did a video about this so I
can just reuse the same graphic that I
had here. This is basically what it
does. If we have a incoming API request
to our NexJS back end it will be caught
by this catch all that's what it's
called. So every request to our app, no
matter where it comes from, will always
go to the slug and then we can forward
it to the Eligia router that should
handle the logic to do this with
built-in middleware with built-in type
safety extremely fast. That's it. Right?
So this is a catch all route. Basically
meaning any request that we have in our
app, let's handle it through this slug.
And that's all this is for now. Right?
Inside of this route.ts, Yes, we can
just grab the Eligia code right here.
So, I recommend you also pull this page
up. This is under iliajs.com and then
the Nex.js integration and then paste
this code in. The reason I paste this in
and we don't write it oursself is
because if anything changes in the
future in Elysia, I want you to have the
more up-to-date code. I think it will
remain very similar to this if it
changes at all. But if you're watching
this video in two or three years, who
knows? Then this should still work. So
we're going to paste this over and then
take a look at this step by step right
now together. So first things first,
we're going to get an error because we
need to install this package. So I'm
going to say bun i elysia. You're going
to say npm install elysia whatever
package manager you're using. And we're
going to see great the errors are gone.
And this is how elusia looks like. We
can define an API route just like this.
This is the same thing as any other
Nexus API route by the way like a get or
a post API route. So for you to really
understand the benefit here, let's see
what it would look like if we defined an
API route in standard NexJS. So for
example, if we wanted to have a user
API, you don't need to follow along
right now. I just want to show this to
you. What we would normally do is create
a user folder, a route.ts inside of that
user folder. And from here we could say
export const get is equal to an error
function where we return a new response.
Inside of this response we have to say
JSON.stringify
and then we could send back the user
with the name of John
for example. Right? So for every piece
of logic we need a separate folder then
a file then we need to stringify and
also this is not even type safe on the
front end. Right? Does this work?
Technically, yes. Let me validate that
with you by opening up my or actually we
don't need a HTTP client for this. We
can just curl this. So, if I just open
the terminal and zoom in a lot here so
you can see better. Here we go. If I say
curl and then http
localhost 3000 / user, then we're going
to see the user of name John. We get
that back, right? So this works
technically, but it's a really ugly way
of doing things and also really
complicated. The better way to do this
is with Elia. So let's delete our user
route I just created to demo you this.
And we can just do this, right? If we
get the slash user, we can just return
let's say for example the user of name
John. That's it. We can delete the other
endpoint. We don't even need that for
now. And now if I do the same query,
bam, we're going to get user name John
but in a fully type safe and much faster
way. And it's also a lot cleaner in the
syntax because instead of a new file,
new folder, it's just a chained function
that we can call on the new eligia.
Right? So that's literally it. This is
really, really, really handy. And I want
to show you how this connects to our
client because right now this is on the
server. We can actually call this API
type safely from our React front end.
And that's the amazing part of EIA. So
how this works is let's kind of open
this up side by side again. How this
works is
uh we can skip all of this with Eden.
Eden is a end to end type safe client
similar to TRPC but a lot more
lightweight. So basically what we can do
is set up Eden. First thing we're going
to export the type of the app. So
basically what this is this export type
app is equal to type of app. If we hover
over this, we can see this is the
TypeScript type of our literal entire
back end like which routes exist. User
get what do they return like name John.
This is step number one of two to
something absolutely magical because
just by exporting the type and
installing one more package which is at
Elyssiajs/Eden
that we're going to install right now
together. Let's clear this bun. I
eliged.
Here we go. And we can start back up our
development server. Here we go. Oh, I
actually already have it running. Never
mind. We don't need to start up two
development servers. We can just close
out of that one. Anyway, after we're
done installing the package, we can just
copy this code over from lib/eden
and create a new folder in our app under
source. Let's create a new folder called
lib. And lib stands for library. By the
way, it's a convention in software
development. Very very common if you get
a job in software to call things lib.
Basically, it means we are preparing
libraries to be used in our project. So,
lib means library here. And we're going
to create a we're not going to call it
Eden, right? Let's call it client
because that's more conventional. I
prefer this naming. They suggest we call
it Eden. Let's name it client though. I
think that's a better way to name it.
And then we can just go ahead and paste
in the code. Beautiful. And let's just
give this a little bit more space. Here
we go. Now we're going to get an error
because we need to um capitalize the A
for app and also insert that here. And
beautiful, the client error is now gone.
And just like this, we have a type safe
API we can use to call our back end from
the front end. This is so beautiful. We
can get rid of the comment. And actually
I will rename API to client because
again I think it's more conventional. I
prefer this kind of naming. And we can
now call or backend from the front end
fully type safe. So for example client
do user.get.
And just like that if we save the result
and I'm just doing this to demo it to
you. We know that the name will be John
right and we can do this from the front
end and call our back end this way. This
is magical man. This is really really
cool stuff. So that's the type safe
extremely performant link between our
front end here, the client and our back
end. This is absolutely magical. Cool.
Okay, so we can close out of the client.
That is literally all we need from
Elysia. The rest is all on us, man. And
it's going to be really, really fun to
use this really quick. The second tool
we're going to set up is Tanstack Query.
Tanquery is the absolute industry
standard of fetching data in React. If
you want to get any kind of software
job, you will 99.9% use Tensa query for
that job. In all my software jobs, we
are using TensaQuery. It's such a nice
tool. It's really really good and every
React developer should know this tool.
Period. Um, so we're going to learn it
together right now if you've never used
it before and it's also not hard. So to
install tens query, let's open our app
here on the side just to have a nicer
view. We're going to open up a new
terminal and say bun i or npm install
yarn add at tanstack/react-query.
Here we go. This used to be named just
react query, but eventually they renamed
it to tanstack to make it more
consistent with the rest of the suite of
React kind of packages that these guys
offer. Really, really cracked people, by
the way. Now to set up tens query, we're
going to create one new
file in our app, let's or actually let's
create a folder and let's name it
components. And let's do it under source
and not app. Here we go. So we have app
and we just created a new folder here
called components. And inside of here,
we're going to create a new file called
providers.tsx.
Now inside of these providers, and let's
give this a little bit more space. Here
we go. Let's first define this as a
client component because we're going to
use react context in here. And from here
we're going to say export const
providers is going to be equal to a
standard arrow function. So just any
other react component. But now the
important thing we're going to say const
and then the structure in this kind of
array syntax here with these angled
brackets the query client from equals
use state. Here we go. And inside of
here, we're going to pass a callback
function that returns a new query client
class instantiation
just like this. Now, this might seem
complicated, but essentially what we're
doing here is on every render, we are
regenerating a new query client. And the
reason is so it never gets stale across
renders. This is the um tanstack query
documentation way, the right way of
doing this. If you don't do it this way,
you might just run into bugs. Um, and
trust me, I painfully debugged like one
day and this was the problem. So, this
is the right way to initialize tens
query. It's just really good to know.
And then from here, we're going to
return a query client provider.
Basically, a context provider from React
query. And we just need to pass it the
client here as the query client as a
prop. And inside of this query client
provider, we're going to render out the
children. Because this providers is
going to be a React component that takes
children, we can just dstructure the
children from the props and type them
out like children react. React node.
Here we go. This is a built-in React
type. Basically, the children are
nothing else than React components. Bam.
Let's save that file. That's it. we can
exit out of the providers and just
render them in our root layout. So what
we can do is let's open up our root
layout and inside of the body wrapping
the rest of our app, the entire app, we
are going to render out the providers.
Here we go. So we can provide context to
all of the components throughout our
application. Perfect. That is it. We did
all the setup work, right? I realize,
you know, setup is not the most fun, but
this was really important to do. We can
close out all of our layout and we can
now actually get started with a really,
really fun part, which is using React
Query and Elia together. And trust me,
this is magical. So, for example, let's
create a way for a user to create a new
chat room. Right? If you click create
secure room here, what should happen?
And let's create a new router called
rooms. So we're going to say const rooms
is equal to a new eligia. Here we go.
And this will get a prefix of slash room
or yeah, let's do room in the singular
and not plural. Here we go. And this
router will get a post route to the
index. So to slash. So whenever you call
slash API/ room and make a post request,
we want the following function to run
just like this. Beautiful. Okay, for now
we can just for example console log
create a new room. So I can demo you how
this works. And to connect this router
with our main app or main backend, we
can just say dot use rooms. Here we go.
So if I open this up more, you can
probably see this easier. The app is a
main router and we use rooms. So when we
now make a request to slap API / rooms,
this post function will handle it.
Beautiful. So let's use that on the
client side to see if this works
correctly. Let's hope head over to our
page.tsx and let's like put this on the
side here. And let's say on our front
end const and then an empty object we're
going to dstructure later is going to be
equal to use mutation.
Now what is use mutation? Well, it's
something that comes from react query.
And right now we don't get any
intellisense. So I'm going to say
developer reload window. If you just
installed a new package and sometimes
the automatic imports don't get
suggested to you, reloading the
TypeScript server in the window probably
here we go fixes the problem. And I
opened that tab by the way with shift
controlp and then reload window. That's
how I did it. And often times it fixes
the import issue. So it's really handy.
Inside of here we're going to pass a
mutation function. So basically all this
means is if we call this hook then this
function is going to execute. So
whatever we put inside of this mutation
function like for example going to the
network boundary to our back end and
doing something react query this hook is
going to handle all the loading states
automatically all the error states
automatically. It's just really really
helpful. So inside of here we're going
to say const res for response is going
to be equal to await client which comes
from or http client dot and we already
can see the intellisense on here. Now
dot room dotpost and we can just say
parenthesis to invoke that function. Now
this is beautiful right? But we can do
one more thing to make this even more
semantically clear and which is in or
route. We could say for example here
slashcreate
because Elia is so type safe. If we
adjust the string right here of if we
post to the slashcreate route it will
know in the front end that we need to
passcreate as well. Right? So whenever
we call this from the front end the
client.room.create.post
post. What it actually does is make a
fetch call to our back end to the same
API route we have defined right here and
it should execute the create a new room
console log. Right? So all it does is
type safely very efficiently very fast
call or back end this line of code right
here. Beautiful. So let's save that and
see if this works. To see if this works,
let's dstructure the mutate, which is
the way to invoke this function, and
call it create room, right? Because we
can name it whatever we want. In this
create room, when do we want this to
happen? Well, basically, when you click
the create button right here, and let's
go to or oh, unable to connect. Did I
stop my development server? Anyways,
let's add the on click right here to the
button. And we're just going to call the
create room function. Here we go. And it
seems like I stopped my development
server. Here we go. So if we now click
try again, it will say waiting for local
host. Perfect. Okay. So here we go.
Let's put this to the side. If we now
click create secure room, what we expect
to happen is to see a console log on the
back end saying create a new room.
Perfect. So what just happened is if we
open this up full screen whenever we
click the button it will make a network
request right here a xhr fetch f fetch
request right here to our back end right
so this is what the eligia client on the
front end does basically it just calls
our back end man and it's really really
nice way of doing that perfect now okay
what should happen in the actual logic
of creating a room well we need some
kind of back end, right? We need some
kind of database to keep track of which
rooms exist and also how long a room has
to live before it self-destructs, right?
And by far the easiest way to do that is
in Reddis. And so I'm going to go to
upstach.com. Now, full disclosure, Upst
is a company that hosts Reddus for very
cheap. It's really, really good. But
full disclosure, I work here. So,
they're not paying me to make this
video. I chose to make this video. Um,
you know, it's just not sponsored, no
nothing. But I want to tell you, hey, I
work here. You know, the reason I work
here is because I like what we do
because I think the Reddis here is
really, really good. I just want you to
know, hey, you know, so you nobody calls
me a shill or something. I want to be
very transparent about this. I also work
here, but it's just a really really nice
service that I used myself before I even
worked here. Um, so I feel very
confident to recommend this for a video.
If you want to go with any other Reddus
provider or self-host Reddus, it's open-
source software. You are absolutely free
to do that, man. Uh, I'm not going to be
saying you should use Upstash. I'm
saying this is probably the best and
easiest way to set up Reddus. Cool.
Okay. If we log into Upstash using
Google, GitHub, whatever we want, we can
create even on the free plan a new
database and let's call it real time
chat. Here we go. We can select any
database for our Reddis. Let's select
Frankfurt Germany for me. And we can
just hit next. And if you're on the free
plan, you can just stay with a free plan
here. I will go with pay as you go and
hit next. Here we go. And I think 10,000
commands per day are free. anyways,
which is more than enough. Um, so you
know, it's a very very big free plan.
It's way more than enough for us to
actually try everything out here and
even deploy to production and get first
production users. Absolutely no problem.
Cool. So that's it. We now have a hosted
Reddis database and all we need to do to
connect from our app is to paste these
credentials here, these tokens and or
the token and the URL inside of av
file. we can create in our project. So
let's quickly go ahead into our root
create aenv file. The enenv allows us to
basically paste sensitive values that
are not deployed with our application.
Right? So uh this is where very
sensitive values like these passwords
here go for our database in avoation.
Let's go back to our app just to have
that on the right hand side. Let's
install the upstreddis package. So
button i npm install at upstreddis.
And this is basically a uh npm package
that allows us to connect to a reddus
database via rest. Right? So it's like a
io radius is for TCP. App-redd is for
rest. It's perfect for nexress and
serverless basically. Cool. Let's start
back up our development server. And now
we are actually good to go with
everything related to creating this chat
room. So what we're going to do is first
generate a const room ID. Every room
needs some kind of identification. And
to do this we can just call nano ID.
Right? That's closed out of the
IntelliSense. Takes up a bit much space.
Here we go. Um so we're just going to
generate a random ID for this room. And
after that we're going to say await. And
now we want to connect to Reddus, right?
We want to make a Reddus call to create
this room in our database. How do we do
that? The easiest way is inside of
source and then our lib folder because
we are after all just preparing the
up-d.ts
Yes. And inside of here we can say
export const radis is equal to radis
from up reddus dot from env.
Basically what this dot from env does is
if you pasted these credentials from
upstreddis will automatically read these
two from our environment file and
provide us a valid radis connection.
That's it. You could pass them like this
new radius and then the URL and token
from your environment variables but this
is just an easier faster way to do it.
Here we go. So we can now say await
radis and import that from our lip
folderh
set and the h stands for hash which is a
reddis data set like an object and we're
going to set one value or two values of
that hash. Now let's give this hash a
name of meta and then the room ID
because it will store meta data for this
chat room and it will store two pieces
of data. The first one will be the
connected and this will be an empty
array which users are currently
connected to this chat room. It can only
be two, right? So that's how we track
that. And the second one is created at
and that's going to be a date.nell Now,
very very helpful piece of metadata to
have later to see when is this room
going to expire. Now, there's going to
be a syntax error here because this
needs to be an async function. Here we
go. And just like that, we can write
into our database.
The last thing we're going to do is
implement the auto deletion, the auto,
you know, busting or whatever we call
it, the self-destruction of a room. And
that's really easy because Reddus
supports expiry out of the box. So we're
going to say await reddis.expire
and we're going to say meta and then the
room ID. So we're going to say this data
that we created in this call should
expire after exactly and let's name it
room_tttl
time to live in seconds as a constant
all uppercase. And let's define that
constant way above our file. And let's
make it 60 time 10. So 60 seconds time
10. So 10 minutes. So each room will
live 10 minutes. If you wanted any other
duration or even let the user change it,
whatever you want. You know, this is the
way to do it. Cool. And then all we're
going to do is return the room ID from
our API route. And you notice how easy
this is. this automatic or Elia
automatically serializes JSON. You know
in Nex.js we would have to do this
return new response and then the
JSON.stringify
and put the object in here and so on. In
Elia we can just do this and it's so
much easier and so much more beautiful.
Perfect. So whenever we now click create
secure room what we expect to happen is
for there to be data inserted into our
upstat database. Right. So let's kind of
open this up full screen here. Go into
our data browser. Here we go. Right now
there's no data in our upstairs database
of course, right? But if we go ahead and
click create secure room and we can also
see the network right here. Create
secure room. We can see the call was
successful. Got a status of 200. So we
now should see the meta of the room in
our database. Beautiful. So whenever we
click a button on our front end, actual
data is implemented or added to our
database. And the only thing missing is
actually sending the user to that chat
room now, right? Because the chat room
is created successfully, right? We can
actually see it in our database. All we
need to do now is inside of our front
end right here inside of our mutation,
we can just send the user to that chat
room. And the way we do that is with a
hook that's built into Nex.js JS called
con router is equal to use router. This
is a builtin nexjs hook we can use to
send the user to certain pages or URLs.
So with address we can say if the rest
status is equal to 200. So this is the
HTTP status code of 200 meaning
successful everything worked. Then we
can just say router.push push and we're
going to push the user to slashroom
slash and then dynamically insert the
res.data data room id that we get fully
type safe from the Edigia front-end
client because we know we're sending
that room ID right here on the back end
to our front end and this is how we know
right this is beautiful. If we go ahead
and save that and click secure or create
secure room again, what we expect to
happen is to now be sent to a 404 page,
right? Because the room doesn't actually
exist yet. But let's click that create
secure room. And if everything works,
perfect. We are now sent to a 404 page
under slashroom slash and then the
dynamic ID we are generating on the back
end. And this will be the actual chat
room. Right? So let's quickly go back.
But we know everything works. This is
absolutely beautiful. We should see two
pieces of data in our database now. Very
very nice. When each room was created
and who is connected to it and now we
can actually go ahead and implement our
chat room. Very very good work, man. So
we can now create secure rooms. And once
we do, it's going to take us bam right
to the room with a 404. Now, of course,
we don't want to have a 404 page for
every room, right? So, let's go ahead
and create a room. And we are going to
create rooms with an Nex.js routing
pattern called dynamic room. So, let me
show you how that works. Let's create a
new folder under source app called room
as the singular. In here, we're going to
create one new folder called in angled
brackets room ID. Here we go. This will
be the dynamic route in a second. And
inside of here, let's create a new file
called page.tsx.
Now, the reason we call this page.tsx is
because that's a NexJS convention. But
the very cool thing you're going to see
happening on the right hand side here is
without me doing anything, Nex.js now
knows, hey, okay, this room page now
exists. Now, there's no actual content
on the page yet, right? So, let's
quickly do that before I can explain to
you what we just did. Let's say const
page is nothing else than a regular
arrow function that we export as the
default at the very bottom here. And we
can just say return p for now saying
hello just so we return something from
this page. And as you're going to see,
we are now on a page where it says
hello, right? And you can see the URL we
used to get here is the localhost 3000 /
room, then the room ID, right? And no
matter what kind of ID we put here, let
me zoom in for you. Right here, no
matter which kind of ID it says right
here, it will always take us to the same
room page. So we can enter any gibberish
here. And once I press enter, it will
always take us to the exact same page
because this is now a dynamic route.
Meaning any ID now corresponds to the
room ID here. And that's great because
we can now dynamically fetch this room
ID inside of this page. Let me show you
what that looks like. First things
first, let's define this as a client
side page by saying use client at the
very top of the page right here. And now
let's actually get the dynamic ID of the
page we're currently on from this
dynamic next.js route. Let's say const
params is equal to use params. This is a
builtin next.js hook. We can simply use
to get the room ID. So for example,
let's say con room ID is equal to params
dot room ID. Right? We can just access
it like that. And we can also type this
as a string so TypeScript knows what's
going on. And that is now the dynamic
room ID that we are on with this page.
So let's save that. And you can see
whatever we input as the room ID. So
let's say for example, hello, my name is
Josh. So it doesn't actually matter
whatever ID is generated on the lobby
page for the button dynamically. We can
now press enter and we now have that
exact thing as the room ID in our page.
Right? That is the beauty of dynamic
Nex.js routes because whatever we
generate on the front end, let's go to
the front end again by removing the /
room. Just going to localhost 3000. If
we now click create secure room,
whatever ID it will generate for us now
automatically we have as the room ID,
right? Because this is a dynamic route.
So this is beautiful. But before we use
the room ID, let's actually make this
look not completely horrible, right? So
from our page, we're going to return a
main tag. That main is going to get a
class name of flex, flex co, a height of
screen, a maximum height of screen, and
an overflow
hidden. Here we go. Let's open up this
main tag. And inside of here, put a
header. A header. Here we go. This
header is going to get a class name of
border B for bottom. A border zinc for
or let's do let's do border zinc 800.
Here we go. That looks better. A padding
of four. Flex items center justify
between and a bg zinc of 900 /30. And
this slash30 stands for opacity, right?
Um, if I had the Tailwind IntelliSense
plug-in enabled right now, I could show
you what this does, but basically we
apply a background zinc of 900 with an
opacity of 30. That's just a cleaner,
shorter way to do the exact same thing
in Tailwind. Cool. Inside this header,
let's create a div. And this div gets a
class name of flex item center and a gap
of four. Inside of here, let's open this
up. one more div with a class name of
flex and flex call. Here we go. Let's
open that div up. And inside of here,
let's create a span that's going to say
room ID. And this span will get a class
name of text xs for extra small, a text
zinc of 500, and let's say uppercase,
which will automatically format this in
uppercase. Boom. As soon as we save
that, beautiful. We have a little header
here saying the room ID. Cool. Right
below this span element, let's create a
div with a class name of flex item
center and a gap of two. And inside of
this div, let's open this up. We'll do
two things. The first one is let's
create a span element with a class name
of font bold and a text green of 500.
And in here, we're going to render out
the room ID so the user knows what room
they are currently in. Right? Beautiful.
Okay. And right below the span element
we just created, let's create a button.
Let's give this button a class name of
text dash and then in angled brackets
for a custom value 10px because we want
this text to be very small. A background
zinc of 800. On hover, we're going to
apply a BG zinc of 700, so one tone
lighter. A padding X of two. A padding Y
of 0.5.
Round it to apply a little border
radius. A text zinc of 400. On hover,
we're going to apply a text zinc of 200.
And lastly, a transition dash colors to
make the color transitions very smooth.
Perfect. Inside of this button, let's
say copy and save that. Beautiful. We
now have a button that technically
doesn't do much yet, but this will be
the button that lets the user copy the
link to the current room so they can
share it with a friend. And this is
actually really easy to implement
oursel. So let's do that. Let's create a
function in our component and let's call
it const copy link. And this is nothing
else than a standard um arrow function
right here. First, let's grab the const
URL, the current URL of this page we're
on. And that's going to be
window.location.href.
So, where we currently are. Then, let's
copy that into the user's clipboard. And
we can use a native web API for that,
which is the navigator.clipipboard.right
[Music]
text. And we can simply write the URL
into there. Then let's display that the
user actually just successfully copied
the value through react state. Right? So
instead of copy, we want this to say
copied. So the user knows they actually
just did something correctly. For that,
let's create a state and let's name it
copy status and also set copy status,
you know. So we have the uh name of the
state and then set the same name just by
react convention is going to be equal to
use state from react and by default we
can set this to copy. Perfect. After the
user has the link in their clipboard
after we put it in. Let's set the copy
status to a string called copied so the
user knows hey this was successful. And
then we're going to set a time out. Set
time out. Here we go. And so we're going
to set the copy status back to copy to
the original value after let's say for
example 2,000 milliseconds, which is 2
seconds. Perfect. And that's all we need
to do. Now, instead of the hardcoded
copy, let's render out the copy status
down here. And finally, let's add the on
click handler to the button saying copy
link. So whenever you now click the copy
button, bam, we should expect the full
URL perfect to be copied to your
clipboard. Very, very nice, man. Very
cool stuff. All right, let's save that
page. And after the button right here
with one closing div and two closing
divs to go, that's where we're going to
open up another area right here. So
there's one closing div, closing header,
closing main after that. Now here we're
going to put a div. And this div can be
self-closing because it's just visual.
And we're going to give it a class name
of height 8, a width of px, which stands
for one pixel width, and a background
zinc of 800. So this will be a visual
separator to the next element that we're
doing to the right of it. Below this,
let's open up a div with a class name of
flex and flex call. Let's open this div
up. And inside of here, a span element
with a class name of text XS for extra
small, a text zinc of 500, and
uppercase. And inside of here, we're
going to say selfdestruct.
So, we're going to show the
self-destruction timer of the room right
here. That's going to be one of the
coolest parts of logic of this entire
app, the self-destruction mechanism.
Cool.
Right below the span, let's open up one
more span with a class name. This class
name is not going to be a simple string
because it will be dynamic. We're going
to use curly brackets right here and
then use a template string so we can
dynamically interpolate here in a
second. The standard class name we're
going to apply is text small font bold
flex item center and gap minus2. And
then dynamically we're going to
interpolate here and not separate by a
comma. This should be still inside of
the template string. Here we go. And now
we're going to do a logic check because
what we're going to have here in a
second is a time that's remaining for
this room. Right for now we can just
kind of mock what this should be. So
let's go to the very top of our
component and let's create a state. This
will be the time remaining and set time
remaining and this will be equal to use
state and this can either be a number or
by default we don't know how much time
is remaining for this room so we're
going to set it to null and just so
TypeScript knows this will be set to a
number later we're going to say number
or null as the generic type for this
state right so what we can do now in the
dynamic interpolation here is really
clever. We can do a logic check based on
the time that we have remaining for this
room. So for example, let's say time
remaining and if that is not equal to
no. So if it's defined and the time
remaining is smaller than 60 seconds
left in that case we're going to say
text red 500 because the room is about
to self-destroy. And in the other case,
we're going to say text amber 500. So
it's yellow to indicate, you know, this
room is not very close to
self-destruction, but somewhat close,
like anything more than 1 minute.
Perfect. And then inside of the span
element, we're going to dynamically
insert something. So curly braces, and
we're going to say if the time remaining
is not equal to null. So if it is
defined in that case we're going to
format the time remaining because we
want it to look good. This is a function
that doesn't exist yet. So we're going
to create it in a second here together.
And we're going to pass it the time
remaining. And else we're just going to
render a default placeholder for the
time which will be dash colon dash to
indicate you know this is currently
loading. Now the only thing left is to
actually go ahead and define this format
time remaining function. We can simply
do that at the very top of our file even
outside of the component because this
doesn't need to live inside of the
component. Let's define a function
format time remaining. This takes the
amount of seconds we have remaining as a
number. And inside of here we can simply
say the const mins the minutes are going
to be math. floor seconds divided by 60,
right? So very very simple maths here.
And then the const seconds is going to
be equal to seconds and then modulo 60
right here. So whatever is left from the
minute. So if it's like um oh and let's
call that sex. Here we go. So we have
mins and sex. So we avoid a naming
collision with the seconds up here. So
this is very very simple stuff. If for
example we have 121 seconds then this
will be two obviously so divided by 60
and this will be the remaining one right
so the remainder of 120 and 60 is going
to be equal to one perfect very very
simple math here and we're going to
return a template string and basically
that's going to be in the format that we
want so this is going to be the minutes
then colon and then the sex dot to
stringpad
start to just format this nicely and
we're going to apply to zero right here.
So a2 and then this string of zero and
that's just going to take whatever time
we have remaining. Let's say for example
121 seconds. Let's put that inside of
the state just so I can show you. And
that's going to end up as 2 minutes 1
second in amber text, right? And if this
was like 51 seconds, it's going to be in
red. So the only thing we now need to do
is to actually update the time based on
the actual time that the room has left
to live. But all the formatting, the
design, the layouting we now have. And
it looks really, really nice, man. And
now the last element of the header is
going to be after this closing span
after this and this closing div as the
last element still inside of the header.
So one closing header, one closing main
to go. In here, we're going to open up
one final button. And this button is
going to get a class name of text XS for
extra small. A BG zinc of 800.
Here we go. On hover, we're going to
give it a background red of 600 because
this will be a destructive button. A
padding X of three, padding Y 1.5.
Rounded a text zinc 400. on hover a text
of white. A font bold transition all
group flex items center.
And there needs to be a little space bar
here. Here we go. After the item center,
we're going to give it a gap of two. And
on disabled, a opacity of 50. And this
button is going to say destroy.
Now, here we go. And right above the
destroy now we're going to put one span
element. And this will get a class name
of group minus hover animate dash pose.
And we can just put a little emoji in
here. So for example, let's use the bomb
emoji. If you're on Windows, you can
just copy this from the web. If we save
that, nice. There's a little, you know,
posing bomb on the button now. And
that's the button that's going to
destroy a room immediately. Beautiful,
man. Very, very cool. So, that's all the
groundwork done for each room, right? We
have an ID that we can copy. We have a
self-destruction mechanism. And we have
the option for a room to destroy
immediately. Very, very good stuff.
After the header, so just with a closing
main tactical, let's open up a div. And
this gets a class of flex one overflow Y
auto padding four space Y4 and a scroll
bar thin. Here we go. This div will
later on contain all the messages sent
in this room. But right now we don't
have a concept of messages. And to you
know allow a user to write a message
first things first we need an input
right? So it makes sense to do the
messages part after the input part. So
let's do the input first. Let's allow a
user to write messages. We're going to
do that right above the closing main
here where we're going to open up one
more div with a class name of padding 4,
border T for top, border zinc 800, and a
bg zinc 900/30
for opacity. Let's open this up with one
more div with a class name of flex and
gap 4. And let's open up that one with
one final div with a class name of flex
one relative and group. Here we go.
Okay, let's open up this div with a span
element. Inside of the span, we're going
to put two curly braces just like that.
So, we can input a HTML like opening
bracket. This is just visual, but it
does look pretty cool. Here we go. And
this span will get a class name of
absolute left for top 1/2 minus
translate minus y minus one/2. So we're
going to center it a text green of 500
and animate pose. Here we go. So it just
looks like a little dev input. I really
like this kind of aesthetic. Right below
the span element, let's put an input.
And this gets a type of text. Yep,
that's cool. And let's give it a class
name of width full. A background black
border border zinc 800. A focus border
zinc 700. Focus outline none to disable
the defaults. A transition
colors, a text zinc of 100, and a
placeholder
text zinc of 700. Let's also give it a
padding Y of three, padding left of
eight, padding right of four, text
small. Here we go. And let's hit enter
or save on that. Beautiful. Let's
disable this nextJS icon because that's
just annoying.
Here we go. Uh, hide dev tools for the
session so we can just get rid of the
ugly ass next icon. Here we go. And
that's our chat input. Very, very cool
stuff, man. We can also give this a
autofocus property. Not auto capitalize,
autofocus. Here we go. So when we load
the page, this input is automatically
focused. That's just really, really
useful. Cool. And then right below the
input and one more closing div with two
closing divs to go, we're going to put
the button to submit this input. So this
button is going to say send and let's
give it a class name of BG zinc 800 text
zinc of 400 padding X of six a text
small font bold on hover a text zinc 200
a transition all disabled opacity 50 a
disabled cursor not allowed and lastly a
cursor pointer here we go and hit save.
So we can see there's a beautiful send
button now we can use to submit this
message. Cool. Okay. Now when we hit the
send button right now nothing actually
happens. So I think the smartest thing
we can do right now is actually keep
track of the input that the user is
making here just through react state.
Right? So let's scroll up a bit to the
top of our component and let's define
one more state which is going to be the
input. Oops. And set input right here.
The input is going to start as a empty
string and we can now use this input to
turn um this text box into a controlled
input. And while we're at it, we're also
going to define a ref right here. Con
input ref is going to be equal to use
ref. So we can programmatically put
focus to this element after you type the
message. And this ref will be of type
HTML input element. Here we go. And we
can default it as null before the
rendering cycle the first one has
completed. Beautiful. So let's turn our
input into a controlled input. The value
of our input is going to be the input
state we just defined on change.
Whenever the user writes something into
this input, we receive the event and
then set the input to the e.target.
Here we go. So just like that, this is
now a controlled input field. Beautiful.
One more quality of life thing when you
press enter here the message should be
sent as well. So we're going to say on
key down just for accessibility here we
get the event and if the e key is triple
equal to enter and we have an inputtrim.
So basically any input is defined not
just spaces or nothing. In that case
let's open up this if statement. We're
going to do two things. The first one is
going to be we want to send the message
to the back end, right? I'm just going
to comment this in to-do send message
because right now we don't have a way to
send the message. But the other thing is
we want to set focus back to the input,
right? So after you press enter, if you
want to write the second message, the
focus should still be in this input
field. So we can say input
ref.curren.focus.
Here we go. And this is a function we
can just call on the input. So when we
type a message, hit enter. Right now
nothing happens. But after we implement
the send message function, of course,
we're going to set the focus back to the
element. Beautiful. And the absolutely
last case we need to handle for the
input. If you didn't type anything,
let's just give it a placeholder. And
that can be type message dot dot dot.
Here we go. Just so it looks a bit
nicer. Very, very cool. Okay. I think
now, man, it's the right time we go
ahead and actually implement the message
functionality. And that's going to
happen on the back end. So, inside of
our Elia back end, right, we're going to
have a bunch of methods to send
messages, read messages, and so on. But
logically, there's one thing we need to
consider before implementing the message
logic, and that is who is allowed to
join a room. It's a maximum two people,
right? A maximum of two people should be
able to enter a chat and no more. So,
how can we enforce that limit? Well, we
can do that in the brand new Nex.js
proxy, right? So, this used to be named
middleware. Now, it was renamed to
proxy. And basically, if we define a new
file right here in our source folder at
the same level as the app folder and
call it proxy.ts, TS this is basically a
middleware that can run whenever we want
when we define a case we want to run it
on right so for example let's say export
const proxy and this is nothing else
than a arrow function and we can
determine when this function runs by
creating a matcher. So we can say export
const config is equal to an object.
Nex.js automatically reads this when it
compiles our code to know when this
proxy should run. And inside of here, we
can define a matcher that defines this
proxy. This code should only run on the
server side when the user goes to
slashroom slash and then any path,
right? So, colon path star means if they
go to any room, then we want to run this
serverside function before it to check
if the user is even allowed to join this
room.
Right. And we automatically get the
request from next.js. Next request is
the type of that to determine who is
making the request and are they allowed
to proceed or not. Right? So basically
what we want to do in here as an
overview I'm just noting this down you
don't need to follow along right now is
check if user is allowed to join room.
If they are, let them pass. If they are
not,
send them back to lobby. Right? So, if a
user tries to join a chat between two
other users, of course, we want to send
them back to the lobby before they can
ever read any messages of that chat. And
this code will be executed before. Um
and only if we validate the user here
and authenticate them then they are
allowed to actually join the room and
read the messages and so on. Cool. So to
start with this let's start with const
path name
is going to be equal to rec.next
URL path name because we want to know
which room is the user trying to access
right so if the URL is localhost 3000/
room slash
Josh room for example. What we care
about is this ID of the room and we can
extract that from the path name. So we
can just write a regular expression to
parse that. We can say const room match
is equal to pathname
match. So we are matching it against a
regular expression. And for example the
reax we're going to put in here is a I
don't know what this called on English.
a carrot, I think, like a roof thing.
This is just how we write a regular
expression. And we're going to do a
backslash, another slash, then room,
then a backslash, another slash. So, you
know, this syntax seems weird, but this
is just how you write regular
expressions. And then in parenthesis
we're going to put angled brackets a
carrot slash and then after the angled
brackets we're going to put a plus after
the parenthesis a dollar sign and I
promise that's it. Oops, I just did that
on accident. So basically what we're
doing is if we have this kind of
statement right here, we're matching it
against this regular expression from
beginning to end. That's what this
means. And we only care about the room.
Great. And if we do not have a room
match, in that case something is wrong,
right? The user is trying to access a
room but or reject didn't match. That
doesn't make a lot of sense. So
realistically, this case won't really
happen. But if it does, we will still
handle it just as a best practice. We're
going to say return next response that
we can import from next/server.
redirect and we're going to redirect the
user to a new URL leading to slash with
the base of the erect URL. So basically
what we're doing here is constructing an
absolute URL based on the current
request URL and we're sending the user
to the homepage the lobby page right
here. Beautiful. As soon as we save
that, the error is going to be gone
because our proxy is now a valid you
know NexJS compliant definition. And
let's actually extract the room ID. And
that's going to be equal to the room
match at the index of one. So that's
going to be Josh room for example. Or if
the ID was this right here, the room ID
would correspond to this room ID. And we
don't care about anything else in the
URL. Perfect. Now to know who is allowed
to connect to this room and who isn't,
we're going to need access to the meta
data for this room. So let's get it from
Reddus. We can say con meta is equal to
await radis let's import that and in
order to use await we need to mark this
as an asynchronous function perfect
radis.h H get all. So all properties of
this hash and the item that we want to
get from Reddus is going to be in a
template string meta colon room ID. So
exactly what we called our data because
if you remember if you go to upstach
right here and reload our page. Oh, I
think we deleted this. Let's go ahead to
our lobby and create a new room. Here we
go. Create secure room. Perfect. And now
we should be able to see that in our
database. Perfect. It's meta colon and
then the room ID, right? And we care
about this connected property here
because this contains the ids of the
users that are allowed to connect or
that have already connected, right? And
we want to get that in the middleware to
see who is already connected and can
this user um that's requesting to join
the room actually connect. Perfect. And
because upstreddis is uh supporting
Typescript, this is a TypeScript native
library, we can pass it a generic right
here to let Typescript know the type of
metadata. So in our case, that's going
to be a connected property. So as an
object, here we go. A connected property
which is of type string array. And we
also have a created at which is going to
be of type number. So now we know this
is the type of metadata that we get. And
Typescript is happy. Perfect. If there
is no metadata for this room, again,
something went wrong. We're just going
to copy the return next response
redirect. Bam. Paste it in here. And we
can return, for example, with a custom
error to the homepage. So we could say,
for example, question mark error is
equal to room not found. So later on in
the homepage we can check are we
redirected with a error in the query and
if so we can show that error message to
the user property. Cool. So that's very
very nice error handling which is really
important. Cool. Now the way we are
authenticating user to join a room is
through a concept called tokens or
authentication tokens. These are not
going to be based on like a Google login
or any login. Basically, it's an
arbitrary token that we write to the
Reddus database to know which users are
already connected. The way this works in
middleware is really simple. Let's say
const response. So, what we're going to
send back from this middleware or proxy
is going to be equal to next
response.ext.
And now we can attach a cookie to this
response. So, for example, we can say
con token. The token we're going to
generate for this user is going to be
equal to nano ID. So any arbitrary ID
and we're going to attach it to the
response by saying response docies do
set. And then we can choose any name for
this. For example, x of token to follow
naming conventions. We're going to put
the token as the value and then some
parameters of this cookie. For example,
the path is going to be slash. So for
any request across the whole website,
this token will be sent to the server,
we're going to say HTTP only true. This
is nice for security because just like
that, JavaScript on the client can no
longer read the cookie. So if anyone
ever managed to do an XSS attack on our
website, our cookies would still be
safe. This is just a security best
practice that you'll find for any
important cookie on the browser. We're
going to do secure yes but only if the
process envoenv
is equal to production. So what that
means is when we deploy our app to
production the secure will be true on
local host it will be false because we
don't need that in local development
just on production. Then same site is
going to be strict we only want this
cookie to be sent along on our website.
Beautiful. So just like that, we
attached a O token to this response that
will now be saved in the user's browser
as long as we return the response at the
very end. We can get rid of the um
comments right here. We don't need them.
And now the important thing is great, we
generated the token for the user. We
attach it to the response. But we also
need to mark this user as being
connected to this room now because if
anyone else joins later with a different
token, they shouldn't be allowed to
join. The way we do that is by saying
await radis.h
set and we want to set the meta for the
room ID. So we're updating the room
metadata. What are we updating
basically?
Oops. Here we go. At the end, let's put
an object here. The connected property.
This will now be dot dot dot
meta.connected.
So, everyone who was connected
previously, but also this user with this
current token, right? If this is the
first user that connects, the first user
token is going to be put in here for the
second user, the second token. Great.
Let's save that proxy and let's see what
happens. Let's go into full screen here.
And whenever we join a room, let's go to
our uh local host to the lobby. Whenever
we join a room, bam, then the middleware
should have run and we should be able to
see in our new metadata for this new
room. Bam. We now have this user with
the token of ZR
PM something something. They're now
connected to this room. Perfect. So for
the second user, their ID will be added
and then we know there are two users
connected to this room. But also there's
one problem in the current
implementation because if I reload this
page I just did once, twice, now three
times you're going to see if we go back
into our database, every time the proxy
runs, a new ID is added to the room
connections, right? And we want a
maximum of two. So right now one user
always gets a fresh ID every time the
proxy runs. That's not great. To prevent
that is really easy though. We can just
check for an existing token. Right. So
right after our meta check we can say
constexisting token is equal to
rec.cookies.get
x of token. Right? So this basically
means if the token exists then this user
already has a token an x off token
attached to their browser right and we
only care about the value by the way. So
we can do dot value and there are two
scenarios now that can happen with the
existing token. The first one is user is
allowed to join room because if the user
was in the room previously and just
refreshed their browser in that case
they should be able to join again.
Right? So if we have an existing token
and the meta.connected.inccludes
this exact existing token that means
this user has already been in this room
and they should be allowed to reconnect.
So we can say return next response.next
which basically means allow the user to
take the action they are requesting.
Just allow the user for the next request
which is to connect to a room. But the
other case user is not allowed to join
if their ID is not in the existing
values or if the room is already full.
Right? We already checked for this case
if they're in the existing values. So
the case we need to check now if the
meta.connected.length
length is larger than two or larger or
equals than two because if two people
are connected this should already not
allow the user to join
basically they want to join a room
that's full right that they are not
allowed to join in that case we can say
return next response dot redirect and
we're going to redirect to a new URL for
an absolute URL you already know how
this works now and we're going to
redirect to slash so the homepage with a
error. So, question mark error error is
equal to room dash full. Here we go. And
we're going to base this off the rec
URL. So, if the user is trying to
connect to a full room, we're sending
them back to the lobby with the error
of, hey, this room is already full, man.
What the hell are you trying to do? And
that's it. If we now delete all our data
in the database just to start over. Here
we go. Let's go into full screen. Let's
try this out. Let's go into our lobby by
just removing the whole room thing here.
Let's create a secure room. Bam. And
that's going to take us to the room.
Perfect. We're going to see the metadata
in Upstash with our ID connected. And if
we now refresh the browser, I'm going to
click reload here. Well, the exact same
ID will be in our proxy because we
already have this token in our browser.
And this user can simply rejoin, right?
So if I open a private window here in
ZMP and hit enter then this generates a
new ID. We can check that in our
application storage and then cookies.
Right here we can see we have a cookie
that is the X
token. If we give this some more space
well I can't. Anyway, you can see we
have an X off token that's automatically
generated in our proxy and attached to
our browser with a value of F A HP
something something right. So basically
this is different than our other can
close out of this window. And if we now
go into Reddus, we can see two users
have connected to this room already
which is our main browser and then the
incognito tab I just opened, right? And
if any other browser or person whatever
now tried to connect to this exact same
room. Well, let's try this. Let's open a
new incognito tab. I'm going to hit
enter. And as you can see, we are sent
back to the lobby because this room is
already full, right? We can see here the
error room full right that our proxy
just managed for. So basically it
checked is this user allowed to join the
room by token and if they are not send
them back to the lobby with this error.
Beautiful. So we know our logic works.
Very very cool stuff. And this is a
really simple but really effective O
mechanism. Right? If we take a look at
how this looks architecturally.
Let's see. Let's go here. For example,
we have the user. The user is trying to
access a room. But before this request
even goes through, it now goes to our
proxy
that we just created. And only if the
proxy allows it is the room ever
queried. So no message at all can be
read if you're not allowed to by the
proxy. And that's token based as we just
saw. So if the user request is invalid,
for example, the room is full, the proxy
sends them back to the lobby before they
can ever even interact with the room. Or
let's make this arrow back to the user
to indicate like invalid, right? And the
room is only accessible under two
scenarios. The first one there is room
left, right? So there's only one
connected user in the room or none. So
there's still a space left for the user.
In that case, they will be able to pass.
Or the second one is their token
user token is already in connected
clients. In that case, they should also
be allowed to connect because they are a
user that has joined the room before
that owns one of the spaces in the room.
So just under these two conditions, the
user is allowed to join the room and
otherwise they will be rejected as
invalid. And that's pretty much all the
logic we have, man. It's as simple as
that. and everything else like reading
messages, sending messages and so on
should happen based on your
authentication status under these same
two conditions. You can only read or
send messages under these same
conditions, right? If you are rightfully
participant of that room and the way
we're going to enforce that is beautiful
because Eligia has a concept Eligia of
middleware, right? It's here under life
cycle. There should be something called
derive.
Um, here we go. Where is that? Here.
Derive. Basically, the derive function
in Elia is a backend function to define
a middleware or self. So, instead of for
every API route that we have checking is
this user allowed to do it or not, we
can just write one central middleware
and call that type safely before every
API route. So let's do that. Let's go
into our source app API and then under
slugs and in this same folder right here
create a new file called o.ts. And the
reason I put this here is because it is
semantically related to the eligia logic
here in the route.ts.
So let's create an authentication
middleware that defines is this user
allowed to send a message to this room
or read messages from the same room.
Right? We're going to do that by
defining first things first a custom O
error. Let's say class O error. And this
extends the base error. And the only
thing we're going to change well we're
going to keep the constructor the same.
The message is going to be a string.
We're going to call the main error. So
we're going to say super message. So
this would be the exact same as any
other other error, but we're going to
say this.name
is going to be equal to O error. And
this just makes it easier to check for
this instance of error. And what that
allows us to do is to now define or
middleware in a very clean way. Let's
say export const of middleware is going
to be equal to a new eligia and the name
of this is going to be off. Here we go.
And let's attach a dot error. Here we
go. And by the way, we also need to
import Elia for this to work. And the
error is going to be our off error. Here
we go. and on error which is a separate
function we can chain to this. This
takes a callback function and it
automatically provides us the code and
the set right here in the callback
function. So whenever anything goes
wrong in our middleware which we can
create with dderive which is also
nothing else in a callback function. If
anything goes wrong in the derive and we
throw an error here, we can define the
type of error here in the on error to
make it easy to debug on the front end.
This is a really really nice way to
handle errors in its I want to show you
exactly how it works because it's really
easy. If the code is triple equal to O
error, which is now registered by our
custom O error,
in that case, we're going to say set
status is going to be equal to 401
unauthorized. That's the HTTP status
code for this. And we're going to say
return error and then in a string
unauthorized.
Here we go. So we have one place where
we properly handle the error that can
happen anywhere in our middleware. It
all goes through this and I think that
is beautiful. Cool. So let's actually go
ahead and define our middleware. Now the
callback function I put in here was just
to get rid of the arrows for now. Let's
actually remove that and get started
with our middleware implementation.
First things first, let's define this as
a scoped middleware right here. There's
some other options. I don't want to get
too deep into it. Basically, this allows
us to run the middleware just before the
endpoints that we wanted to run before,
right? This is not a global middleware.
This would just run in front of the
actual endpoints um that we care about.
You're going to see what that looks like
here in a second. And then the second
thing is going to be an async callback
function. Here we go. So now we also get
IntelliSense here. You can see this can
be a local scop or global middleware,
but we're going to go for sculpt.
Beautiful. Inside of this callback
function, we get access to well
basically a lot of things, right? To any
information about the incoming request,
but we only care about two, which is the
query and the cookie, right? So the
cookie to validate um does the user have
a valid token and the query for the
current room ID that user is requesting.
So let's get that information. Let's say
const room ID is equal to query. room ID
and let's also say const token that the
user is requesting this with is equal to
cookie at the index of x of token dot
value right and if we hover over this we
can see by default this is typed as
unknown so let's type this as string or
undefined so it's just easier to work
with great that's the only two pieces of
data we need which room is the user
they're trying to connect to and is
their token allowing them to connect to
this room or not. If we do not have a
room ID or no token in either case, the
user is not allowed to continue because
they need to have both. We're going to
throw a new O error and inside of here,
we're going to say missing room ID or
token. Period. Cool. So after this guard
clause we know both room ID and token
will be present and we can check it
against the metadata of the room that
the user is trying to connect to. First
off we need that metadata. So let's say
const connect connected is going to be
equal to arate radis.h get. So we're
going to get a property from a hash.
Let's import reddis to make this work.
And we are going to put the meta of the
room ID in here. So we're going to get
the meta data. And what is the property
we want to get? Well, the connected.
So this call is basically going to get
us well, we need to create another room
because this data has already expired.
Let's click create secure room. So I can
show you this. Basically, what this call
does is it gets us this connected
property here of a room, right? We don't
care about the created ad. We just want
the connected property. And to make
TypeScript happy, we're going to tell it
this is going to be of type string
array. Great. And now for the very easy
check, is the user allowed to join the
room or not? Well, if not connected
includes this token, then the user is
not allowed to join. If the user token
is not inside the connected array, they
are not allowed to join. So we're going
to say throw new of error and we're
going to say invalid token. Your token
does not allow you to join this room.
And that's it. We can now return
the O object and in here pass the room
ID token and the connected array. So
these values are now accessible to any
API route that we have after this
middleware was executed. Bam. So that's
it. We now have a fully reusable, very
very clean middleware. We can run in
front of anything that should verify
that the user is authenticated to join
and interact with this room. Right? And
if they are not, we're automatically
throwing an error. Only if the user has
been connected to this room determined
by our proxy, this middleware will let
them pass. Very very nice and also fully
type safe. So this is beautiful. So
let's actually go ahead and use this O
middleware in a realworld endpoint.
We're going to do that inside of our
route.ts
or main Elia server. Now logically the
first thing that happens in a room is
sending a message, right? Without that
we can't get messages or anything else.
So we're going to implement sending a
message first and that can only be done
by of course authenticated users. So
let's create an API endpoint to send
messages. Let's say const messages is
equal to a new illusia. And we will give
this a prefix not a pre-ompile a prefix
of slash messages.
Here we go. And we are going to say dot
use o middleware and go ahead and import
that. So any API route we now define on
this endpoint will automatically use the
off middleware before executing its
logic. Let's attach a post to the main
route. So this is going to be SL API/
messages and then a post request and
then the actual callback function that
handles the logic of this because we use
the O middleware before this. We
absolutely know that we can receive the
O object here because this runs before
this code. If we didn't use the O
middleware, there would be no
authentication. But this will always run
before this post request. So we know the
room ID, the token and connected. And we
also know the user is allowed to call
this route because this middleware ran
before it, right? So that allows us in
this logic to assume the user is already
allowed to make this request to actually
post a message to this room.
Security-wise, this is super super nice.
Great. So let's also grab the body and
the call back here and let's just
dstructure both the well from the body
let's dstructure the sender and the
text. So who is sending this message and
what type of text are they sending? But
the problem is we don't know right. So
whenever you try to make a request to
the messages endpoint, you need to tell
me or you need to tell the API I guess
not me who are you who's the sender and
what kind of text are you trying to
send? And Elicia also has a really nice
way to validate that every API request
involves this data. And that's at the
very end here we can open an object and
just type that out. For example, for the
body, we expect a Z dot object. And that
is not the Z add command. It's just Z. Z
is a property coming from the ZOD
package. Probably one of the most
popular schema validation libraries of
all time. It's called Zord. It's a
TypeScript first schema validation with
static type inference. This sounds very
complicated, but if you've never tried
out Zod, let me show you how it works.
First things first, let's install it.
Ban i z or npm install zord. And at the
very top of the file, we can just say
import z
from zord. Here we go. So what zord
allows us to do is to define a schema.
Basically a shape. For example, the
shape of the body. Just follow along
with me here and I'll show you what this
does in a second. The shape of data that
we expect for this API route is going to
be an object that has two properties.
The sender which should be a Z dot
string with a dot max length of 100. So
a user ID cannot be longer than a 100
and a text and that's going to be a Z
dot string of dot max 1,000 for example,
right? So you can't send a message
that's longer than 1,000 characters.
When you now make a request to this API
endpoint,
you cannot send a text longer than 1,000
characters and it has to be of type
string. If not, the API request is
automatically rejected because the user
is trying to abuse our service, for
example, with some data that we are not
expecting. The beautiful thing about Zot
is before any of this code runs in the
actual post endpoint, we automatically
validate the request body against the
shape and only if it exactly matches the
shape, this code will run. So the
beautiful side effect of that is we
absolutely know the sender will be of
type string and the text will be of type
string when this API logic runs. We
validated the incoming data. This is
also awesome for security, right?
Because if the user tries to send some
data we don't expect or tries to, you
know, abuse our API endpoint, it they
don't send the text, for example, they
just sent the sender. We will
automatically reject their request
saying, hey, you missed this text. You
didn't send that only if it's valid data
exactly as we expected. Then our logic
will run. So we know the exact type of
data that we're working with in the
actual API route. Right? This is really
really useful. And we don't only want to
validate the body. We also want to
verify the query and we want to have a Z
dot object in there with a room ID.
That's going to be a Z dot string. So
let me show you how that works. Let's
hook up that endpoint to our main Elysia
by saying use messages. Here we go. And
just like that, we have a API endpoint.
If I now show you what that looks like.
Well, let's go in here. Let me open up a
terminal and let's say curl minus x
post. So, I'm making a post request to
this endpoint. http
localhost 3000/ API/ messages. And I hit
enter. Missing room ID or token because
first things first, there is no room ID
in our request or no token. So, our
authentication middleware automatically
rejected this request because we never
went through our proxy. Great. This is
of course great for security, but I want
to demo you this. So, let me quickly
comment out the off middleware. You
don't need to do this. Let it let it
there. Um, I just want to demo you the
actual verification here from Zod. If I
try this again now, Zot rejected or API
request message invalid input expected
string received undefined errors um
invalid input expected string received
undefined for the path of room ID. So
what it's basically telling us we forgot
to add a room ID is equal to my room for
example to or request right if it
doesn't exist or message or or request
is rejected. So now, oh that didn't work
because I forgot quotes around this URL.
Here we go. If I now make a post request
to the same API endpoint with the room
ID, then we didn't pass a body with a
center of text, right? So it's going to
tell us invalid type expected object
received undefined for the body right
here. And only I think you get the
point, right? only if our API request
involves exactly this and the query then
it is actually processed by our API. So
we can be absolutely sure of the data
that we are working with which is
extremely extremely useful. So let's
comment back in the O middleware because
of course we want authentication in our
app. Great. Let's comment that back in
and let's actually continue in our main
logic. So the first thing we want to
check in the actual API logic is if the
room even exists that the user is trying
to send a message to. So we can say con
room exists and that's going to be equal
to await radisexist.
So we can see if a certain data point
exists in our database and also let's
mark this as async so we can perform an
awaiting operation. I'll also pull up
our room here just so it looks nicer.
Cool. All right. The data point we want
to check for is meta room ID. So we're
going to see does this exist or not. And
where does the room ID come from? Well,
from the O, right? We know we
authenticated the request. So we can
just grab the room ID from there. And if
not room exists, the user is trying to
send a message to a room that doesn't
even exist, we're going to say throw new
error. And inside of here, room does not
exist. Hell yeah. Very, very nice. Cool.
After that, let's create the message
that the user is trying to send. All
right, man. Sorry if there was a little
cut here. I just took a break and it's
actually the next day for me. Um, so if
some things look a bit different in my
browser or in the code, um, you know,
that's because I just cut the video.
Anyways, now comes the part in the
entire project. I am most hyped about
because when you send a message in a
room, everyone else should see that
message in real time, right? Let's hide
the NexJS dev tools here. So when I send
a message, everyone connected to this
room should see the message in real
time. So exactly when it's sent, how do
we implement that? And now comes the
coolest part, I think, of this video.
We're going to use Upstash real time.
Upstash realtime. This is not sponsored
by Upstash, by the way. It's a package
that I made. I own this project. So, I
made this. I work at Upstash. I created
real time. I think you got my point,
man. Anyways, it's it's finding good
adoption, man, because it's genuinely
the easiest way to implement real-time
functionality into any app. It's
seriously good, right? It has firstass
TypeScript support. It's extremely fast,
has zero dependencies, and it's super
small gzipped. It works on Versel
natively. It's completely type- safe
with Zot 4. So the most modern version
of Zot. It's genuinely the best way to
do real time and I want to set it up
together with you. So what we did, I
wrote some documentation for upstre time
and we're going to set it up together.
It's kind of like a open-source pusher
alternative, right? So we already have
up-ash reddis and zot in our project. So
to implement realtime messaging the only
thing we need to add is bun ii npm
installed yarn add at up stre
and I want to show you how nice this is
to use because that's all we need to do.
That's literally it. We need to install
real time. We already have the reddis
database set up here. So we can skip
that entire part of the documentation.
And then let's create our real time file
that defines the events that can happen
in our app in real time. So under lib
for library, let's create a real time.ts
um file here. And from here we can just
go ahead and copy and paste in the
documentation. But I want to write this
together with you because I think it's
easier to understand what's going on. So
the first thing we're going to do is
define a const. Oops. And let's disable
the autocomplete a const schema in this
file. And in here we can basically
define the events that can happen in our
app. And didn't I just disable cursor
tab? Here we go. So in our app we want
something called a chat event. And
whenever a chat event occurs, well let's
give it the type of data that we expect
to send. So there's going to be two
scenarios that can happen in our app.
First, what do we want to transmit in
real time? A message, right? If a user
sends a message, then that should be
sent in real time. The other one is a
destroy event. So, when a room is
destroyed, all other people need to know
about that in real time to be
disconnected from the room. Right? So,
those are the two events that we want to
have in our app for real time. For the
message, let's type this as a Z.object.
So we can use zot here. Let's get rid of
this to properly type the schema to be
100% type safe. Each message will get
the following properties. A id which is
going to be a z dot string. Let's give
each message a sender also a z
dotstring. The username of the person
that sends this message. The text
zstring. You can probably guess what
this is. The actual message content.
Let's give it a time stamp as a Z dot
number when this message was sent. A
room ID Z dot string which room was this
message sent to. And finally a token who
sent this message. And that's going to
be a Z.string dot optional because for
security reasons we are allowed to omit
this token from sending it back to other
clients in real time. So this should be
optional to not leak any security
credentials later.
Great. And then for the destroy event,
let's just say destroy. And this will be
a Z dot object. And each destroy event
should have a is destroyed property of Z
dot literal true because technically we
don't really care about the data that is
sent in the destroy event. We just care
about the event itself. So technically
it doesn't even really matter what we
put here. But what we need to do is to
be able to send a real time destroy
event to all connected clients.
Beautiful man. That is almost it. Let's
use our schema and say export const real
time is equal to a new real time that we
can just import from uprealtime
and we can pass it or schema and also or
radis instance. So if you give this a
bit more space here's how this looks
like. If we import our radius, let's say
import radius from addlibreus.
Here we go. And sort or imports using
shift, alt, and o automatically. Then
the arrows are gone. And we now have a
real time instance. We can use this
real-time instance. Let me demo this to
you. Real time.
And fully type safe. We can emit
messages on the server with the data
that we expect based on our schema and
in real time receive those events on the
client using a special hook that's also
fully type safe that I'm about to show
you. Beautiful. The only thing we still
need to do in this file is let's say
export type realtime events so we can
make this type safe on the client is
going to be equal to infer real time
events. And if you're wondering how I
know all this stuff, like where is this
type coming from? Well, it's all
documented, right? So, you could
theoretically also just copy and paste
from the documentation, but I want to
explain to you what we're doing as we're
doing it. So, we're going to pass the
type of real time inside the infer just
like in the documentation. Now the last
thing we can do is if we want to use
this message type somewhere across our
app. Let's actually just separate this
in a separate schema. So we can say for
example const message is equal to that
exact same thing we just had here. And
just like that here we go. We now have
our message as a separate result schema.
And we can just reuse that. It's the
exact same thing right? It's the same
logic but now at the very bottom we can
say export type message is equal to
Z.infer
type of message. Here we go. So we can
use ZO to infer the type of the message
which is you know ID sender text
timestamp and so on to just get the pure
TypeScript type just from the message.
As easy as that man that's our real-time
file. Now this almost already works. The
only two things we still need to do is
for one to implement the handler. So
under
uh source app, let's create a new folder
under API called real time. And in here,
new file route.ts
and just paste in the handler from the
upstre realtime documentation here. This
automatically handles reconnection
events, message history, everything we
need in a real-time communication
system. It just works. We just need to
paste the code under this API route. API
realtime route.ts. And that's it. We can
already close all of this. And now the
absolutely last thing we need to do is
add our real time provider to the
providers, right? Right. So we can go
into our providers file that we created
way earlier and wrap our entire app or
the query client provider from late from
earlier in real time provider. Here we
go. Oops. And then close that off with
real time provider as well. Here we go.
So we wrapped our app in this real-time
provider. Same thing. We can just close
out of the providers file again. And as
long as we now create the real time
client hook, the use real time that we
can use to listen to these events on the
client, we're done. That's it, right? So
last file under lib realtime-client.ts.
And you're about to see how nice this
is. Let's just paste this code in. Here
we go. So we have the export consime is
equal to create real time. We pass it
the generic which is how we know the
events that exist in our app and that's
it. We have an extremely fast type very
very performant realtime system ready
for our app. This is by far the nicest
way to use real time and I will show you
how to use it right there and now man.
Let's do it right now. So let's
construct the message that we want to
send to all clients in real time in a
room. Let's say const message. By the
way, we're back in our Elysia main route
here in our messages handler. That's
where we are. And each message will be
of type message from real time. So we
know we're not missing any property
here. Each message will get an ID. So we
now get that type safety. And that ID
can be a nano ID we just randomly
generated on the server. Let's give it a
sender which comes from the request and
also the text comes from the request
body as we dstructure it way up here.
Let's give this a time stamp of date
now. So we generate that on the server
and also for the room ID we also already
dstructured that here coming from the O
right from our O middleware. So that's
all already taken care of for us. Cool.
Now we have the message in memory in the
server, right? What do we do with that?
Number one, what we're going to do is
add message to history because we need
some kind of message history. If you
reconnect, of course, you want to see
which messages were already sent in this
channel, right? So what we can do is say
await radis dot and we're going to r
push. We are going to push this message
into a list. So into an ordered list of
messages. So we can just fetch this
again and the messages in this room will
already be in the correct order. Let's
name this messages and then colon room
ID to identify this message history for
this room. And for the data we're going
to put in here. Let's put an object
spread in the message. And also for the
token we're going to use the O.ken.
So we are going to persist the token in
the message history on the back end
because that's not leaking any
credentials to the client. This is safe
to do. Cool. And then to actually enable
real time communication here, we can
just do await real time. Let's import
that dot channel. So we can push to a
specific room only. And let's use the
room ID for the channel. And we're going
to dot emit. We're going to emit an
event. the chat dossage fully type safe
and it already knows the type of data we
can put in here which is exactly our
message. So this is it real time channel
room ID.eit
the type of event we want to emit and
the data we want to go with that. That's
it man. Just like that we now have
realtime messages in our app. But before
I show you, let's quickly enable some
housekeeping here after this. So let's
say housekeeping or whatever. You don't
really need to comment this in. Um but
basically we want this room to expire
after um 10 minutes maximum, right? So
what we're going to do right now is to
enforce that expiration. Let's first get
the remaining seconds of this room. And
we can do that by saying const remaining
is going to be equal to rate reddis ttl
which stands for time to live. So
basically time to live is a concept
built into radius. Any piece of data in
reddis can have a time to live which is
by default infinity. Right? You can see
for this room it's 5 minutes left here.
This is the TTL and we're using Reddus
as the source of truth for this data
because if you consider how we create a
room whenever a room is created, it
already has the TTL of 10 minutes,
right? Which starts expiring on the
server side. So here we're just using
Reddus as the source of truth for how
many of those 10 seconds do we still
have left, right? So let's get the meta
at the room ID. Here we go. And that's
going to give us back like this room
still has 6 minutes for example. So
after that we can say await
reddis.expire.
We can explicitly say hey this key we
want to expire after this remaining
duration. For example let's expire the
messages
at the room ID after the remaining time
just like this. Let's also say await
reddis.expire expire and let's expire
the history of this room ID after the
remaining duration. Here we go. And
lastly, let's say await radis.expire
and we want to expire the room ID
itself.
And that's going to be after the
remaining duration. So this holds
message history information made by
upstream realtime. Whenever we send
something through up session realtime,
it saves the message history in a
reddish stream. So for example, when you
are disconnected because of a network
outage and reconnect after a second, the
Reddit stream knows all the messages
that were sent during that duration and
it can replay it on the client. We also
want to expire that after the remaining
duration.
Cool. And that's literally it, man. We
now have real time messages sent from
our back end. Let's verify that, right?
Let's take a look at this. Let's go into
a room. Let's create a new one just to
be sure that it will, you know, stay
around for at least 10 minutes. Here we
go. And let's send a message in our um
room. Here we go. So, I just hit send.
Let's go. And we can now see a meta data
right here. But the data is not here.
Why is that? Let's go and make this
smaller again. Did I forget to hook up?
Aha, here we go. To-do send message.
That's why it doesn't work. Okay, so we
don't have a way to send the message
from our front end yet, but that's
really, really easy to do. Let's just go
above the copy link function below our
state right here and let's define a way
to send the message because all the hard
part is already done. We did the logic.
Let's do that with react query. Let's
say const empty object because we'll
worry about dstructuring later is going
to be equal to use mutation the way to
make a post request to our server. And
inside of the mutation function, which
is going to be an async callback
function, let's say await client or HTTP
client dot messages dot post. And this
takes some data. For example, let's pass
in the sender. That's going to be the
username of this user and the text that
the user wants to send. Now, this is
giving us a bunch of problems because
for one, it's also missing the query
that we need to pass, right? And that's
going to be the room ID. Cool. And we
get a bunch of errors. So, error number
one, let's fix it. The room ID needs to
be in an object because we expect it as
an object in our back end, right? The
second thing, the mutation doesn't know
where the text is coming from. Well,
let's receive it and pass it wherever we
invoke this mutation. So let's receive
some text in this callback function and
type that out as text string. Right? So
wherever we call the mutate and let's
call that send message there we will now
have to pass the text as a string and
worry about it there. And the last name
or the last thing is the username. Where
is that coming from? And the thing is
technically we already know the username
from our main page. Right? So we
implemented all the logic here already.
We kind of need the same logic here in
the room ID page because what's possible
is that a user doesn't even go to the
lobby. If they get sent a link from
their friend, they immediately join the
room. They didn't even have time to
choose a username in the lobby. Right?
So, the easiest way to go about this is
to learn how to implement a custom hook
in React. Right? Because the logic part
of this is already done. We can just
copy and paste this over and make this
very clean and reusable to this other
page through one centralized reusable
hook. Doing that is super easy. Let's
create a new folder under source and
let's call it hooks. And inside of here,
let's create a new file called use
username.ts.
Here we go. Now, any custom React hook
has to be named use something something
something, right? So we can say export
const use username for example. And this
use here is really important. This is a
necessity in React. It needs to start
with this keyword to be recognized as a
custom hook. Cool. Okay, that's the the
base is done, right? This is already a
custom React hook. It doesn't do
anything, but technically it works,
right? But let's make this actually do
what we want. So, let's grab the
username state from our main page, cut
it, and simply paste it in here. Because
this is a custom hook, we are allowed to
use other React hooks inside of it, like
use state, for example. The exact same
goes for the use effect. We can just go
ahead copy or cut that out with Ctrl X
and go ahead and paste that in or use
username function. We're going to see
one or two things are missing here like
the use effect import. We can just
import that. We can also cut out the
storage key. We can also cut out the
animals and the generate username logic.
So the page is not anymore concerned
with any of the logic that goes into
making username. Just the use username
hook is. And as long as we now import
nano ID in the use username, we are
almost good to go. Right? This is now a
custom hook. It generates a username
when it loads, when it renders. The last
thing we need to do is to actually
return the username from the hook. So we
can just say return username from the
hook. And this is how you make a custom
React hook. It's as simple as this. So
what that allows us to do is now very
easily reuse this logic across our
entire app. We can dstructure from use
username now in the page that we had
before. Just destructure the username
from here. Bam. And we can clean up all
unused imports with shift, alt, and o.
Here we go. So we have cons, username,
use username. As easy as that. We can
now close out of our main page. And the
exact same logic now goes for or other
page, right? For or room page right
here. We can just use username like
this. And bam, we have the username
accessible to this page as well. And now
the send message knows which user is
sending this message. Perfect. Cool.
Let's save that. Close out of the other
tabs and see what happens. Let's open
this in full screen actually just so
it's easier to see. And let's see what
happens when we send a message. Hello
world. And that's it. Send. And well,
nothing happens yet because did I forget
to use the send message? Okay, man. Of
course I did. Anyways, let's go ahead uh
and use this send message in wherever we
put to-do send message. Where was that?
Here. To-do send message. So, whenever
you click enter or hit enter, we want to
send the message and we now need to pass
the text. And that's going to be the
input. So, the React state, we're
keeping track of the value of this
input. Beautiful, man. The exact same
thing goes for the button. If you hit
the send button, of course, we have to
have an on click handler here that sends
the message. So let's say send message
and again the text is going to be the
input. And let's also reset focus to the
input field by saying input
ref.curren.ocus
which is how we set focus to this input
field again. Beautiful man. Very very
good stuff. Just as a best practice, I
want to add one more thing here, which
is a disabled property. If the inputtrim
doesn't exist, so basically input.trim
removes all whites space and if there's
no actual character in it, this will
evaluate to false or to true rather,
right? So this will be disabled. You
can't send the message if nothing is
typed in the input field. Or what we
also want to do let's say is pending.
Now what is is pending? Basically while
we are sending a message while this
network request is in transit it doesn't
make sense to send another message right
because the other one is already
processing. And the beautiful thing is
react query takes care of that loading
state of that pending state
automatically for us. We can just
dstructure it from the use mutation
where we send the message. And that's
it. We can now send a message. Hello
world. And that's going to be sent to
our back end. Cool. And of course, we
get the 500 response. Man, that's the
that's the show effect. Let's see what
the error is. Why we get a 500. Ah, and
we get an invalid token. So, I just did
some debugging. This might actually be
because this room has expired while we
were in it. So, let's create a new room.
We didn't do the room expiration logic
yet, so this might be expected. Let's
try this again. Let's open up the
network tab and send a message. Hit
enter. Oh, nope. This still happens. We
still get a 500 response. This can
happen, man. Let me debug really quick
why this might happen and then tell you
about what the problem was. So, give me
just a second. Aha. And I did some
debugging. I found the error. I
accidentally typed a dot here in or off
middleware and not a colon. So it tried
to find a database entry that doesn't
exist because I just typed the string
wrong. Let's see if that was the error.
Let's delete this. Let's try this again.
Send the message. Send. And here we go.
That was the problem. Very, very nice.
If you didn't make that typo, um then
you already got the 200. Beautiful. So
we actually were able to send the
message successfully. And let's verify
that everything worked by going into our
database. And beautiful, we can now see
in our data browser, we have three
things here. So the first one is the
stream. This is an internal thing for
upstre time to make sure no message ever
gets lost, right? So it has a message
delivery guarantee. We have our actual
message history. So this is the message
number one in that room. And we have the
meta data for this room. for example,
which token is connected to the room
right now. Beautiful. And all of these
expire here in eight minutes. So,
they're all synchronized. They all
expire at the exact same time. So, we
know now it's 7 minutes here. We know
this room will automatically get deleted
in 7 minutes. Beautiful. Now, the
message though doesn't show right now,
right? So, we sent it. It did get
recognized on the server, but it's not
showing in the room right now. And
that's actually really really easy to
implement. So let's do that now in our
page.tsx file. Or actually, you know
what? Let's start on our back end in our
route.ts here and then hook it up to the
front end here in just a second. So how
do we get the message history for room,
right? Let's go ahead and add a get
method to this whole thing here. So we
can say dot get. This is still on the
messages kind of router and let's get
the slash and give this a call back
function. Let's mark this callback
function as asynchronous and also
dstructure or o from here that we know
will be there because the o middleware
already runs before this endpoint.
Beautiful. And all we need to do now is
fetch the message history from Reddus
and it's already in here. We just need
to get it. So we can say cons messages
is equal to await reddist l range. So
we're getting all messages from this
list. And the name of this list is
messages colon and then o room id.
Beautiful. And to tell radius we want
all messages we need to give it a start
of zero and an end of minus one. So
basically no end give us all messages.
And we can also give this call a
generic. So, TypeScript is happy. We
already know that we will get events of
the message type, you know, because
that's what we put into this list in the
first place. Beautiful. Now, as a
security best practice, what we could do
right now is return the messages to the
front end. But don't forget, messages
contain the token of the person that
sent it. So what we want to do is to
only include the token if you are the
person who sent this message. So the
reason here is if we want to display
let's do xcal draw. If we want to
display the messages when we render them
they look like this right all messages
that are from you should be marked as
you and all messages from you know
anyone else them. And this could be like
one person, five people, whatever in a
WhatsApp group chat, Discord group chat,
you know, this can be 100 people. The
bottom line is your messages look
different than anyone else's. And that's
the reason why we want to do some logic
here for this token and only include it
for yourself and not for other people
for security reasons. So what we're
going to do is say messages do map. So
we're going to go through every message
and let's call it M and just return an
object directly here. This object will
contain every property of the previous
message. But for the token, we're going
to check if the M dot token, the token
that is in our database right here. If
that is the same token used to make this
request. If the M dot token is triple
equal to the O dot token, then this is a
message that you send. It's fine that we
include the O token here because this is
your message. You own it. And for anyone
else's message, we're going to say
undefined. So, we're not going to
include their token in a response to
you. Great. So, we did that for
security. Now the last thing we want to
do here is to actually force giving out
let's say query here Z do.object and
this will get a room ID of Z dot string.
So whenever we query this endpoint we
want to force that the user gives a
object that contains the room ID. So
which room are you requesting the
message history for? And then our off
middleware will be happy because it
expects this room ID. Right? Beautiful.
That's it. That is literally the message
history man because all the ground work
is already done. So what we can do now
in our component let's just query this.
Let's say const empty object. We'll
worry about the structuring later is
equal to use query from react query. And
now as the query key we need to give
this a name to identify this function
against the cache because this has a
built-in client side cache. Let's name
it messages and let's make it a
composite key of the messages uh string
and the room ID. So if the room ID
changes and you connect to a different
room, the cache will automatically be
busted and the messages will be fresh
again. This is just to avoid any stale
caches. Um you're going to see why this
is uh helpful later. The more important
thing is the query function. This is the
important part. Let's make this an
asynchronous error function. And in here
we can say const res. So response is
equal to await client dot room dot or
not dot room dot message. Here we go. My
bad. Dot messages dot get and we can
simply as the query pass the room ID.
Here we go. As easy as that. We make a
network request to our back end telling
it, hey, give us the message history for
this room and then we simply return res
data from here. Perfect man. Very, very
cool. This query function will
automatically run on render whenever
this component renders. And to access
the data, we can simply dstructure the
data on top here. And let's call it
messages because that's what it is. It's
just a message history. Beautiful. Now,
where do we use that message history?
Basically, we can simply render it right
here. Right in the section we left empty
earlier to actually show the user
messages. So, let's already save this
and we can go way down here below our
header. Here we go. So, this was the
part we left for the messages. We can
even add like a little comment here
messages where we can just render them
out. So let's do that right now. Let's
do a logic check. If the messages do
messages
length is triple equal to zero, then
let's conditionally render a div element
just like this. This div will get a
class name of flex items center justify
center and a height of fo. Let's open
this up and give it a P tag inside of
here saying no messages yet.
Start the conversation.
Oops, I misspelled that. Conversation.
Here we go. In this P tag, let's give it
a class name of text zinc 600 text small
and font mono.
Here we go. If we reload this and create
another secure room because the old one
expired. Here we go. No messages yet.
start the conversation. Beautiful. Now,
what if we have messages? Let's handle
that case right below here. In the other
case, we're going to say messages
dossage messages.map
and let's call each one a msg, a
message. And let's directly render out
some JSX here. So, at the top level,
let's put a div. Because we're mapping,
this div needs a key that stays
consistent across renders. So, let's
give it the message do ID. Let's also
give this a class name of flex flex call
and items start. Inside of this div,
let's open this up. Let's create one
more div. And this one gets a class name
of maximum width 80% in these angled
brackets for a custom value. And then
let's give it a group as well. Nice.
Let's open this div up. And let's give
this div a class name of flex items
baseline and gap three and margin bottom
one. Here we go. Let's open this div up.
And inside of here we're going to put a
span element with a dynamic class name.
So we're going to use these curly braces
here. So let's create a template string.
And first let's apply a text xs for
extra small and a font bold. And then
let's do a dynamic check. If the message
sender right here is triple equal to the
username, in that case, we're going to
render out a text green 500. And in the
other case, we're going to render out
text blue 500. Here we go. Cool. All
right. And then inside of the span
element, let's open this up. We're going
to do a conditional check. If the
message sender, oops, here we go. sender
is triple equal to the username. In that
case, we're going to say you and in the
other case, we're going to say
message.ender.
So, whoever has the username of the
person that sent that message.
Beautiful. Okay. Right after that, let's
first save that. Actually, you know
what, man? Let's just see if this works,
right? So, we have the message history.
Let's say we are going to create a new
room because this one has probably
expired by now. Let's create a new one.
Create secure room. Here we go. Let's
say hello world. Hit enter. That sent
our message. And if we reload the page
now, what we expect to happen is to see
the full message history. So, we see our
message. Let's open up the network. Hit
reload. And
here we go. We can see there is a
message from you. We can't see the
content right now, but we can see the
message is here because we're not
actually rendering out the content, you
know, but we see this call in our
network tab, which is for the message
history. And we can see we get the full
message also with the text here in the
network request. We just need to render
this out now. Perfect. So, just like
that, we know we did everything
correctly. And now we can just actually
render out the actual text and the time
stamp when this message was sent.
Beautiful. So let's do the time first.
Let's open up right below this span with
three closing divs below it. One more
span element. And let's give this a
classroom of text 10px as a custom
value. So in angled brackets and a text
zinc of 600. Here we go. And inside of
the span, well we want to put the
message timestamp. But if we just put
the time stamp and hit that save, it
looks like this. It's a Unix timestamp,
right? It looks really weird and ugly
and we don't want it that way. So, we're
going to install probably the last
package of this entire project, man. And
that is going to be let's screw this up.
Bun ii date-fns,
which stands for date functions.
Basically, these are utility functions
very very lightweight to make it easy to
work with dates and times. And date fns
has a function called format.
Now, uh, we're not going to get
autocomplete here because,
uh, or IDE is a bit stupid, but I can do
the trick or we can do the trick that I
showed you earlier. We can hit control
shift and P and then reload developer
window at the very top here. And if we
click that, then the IntelliSense is
going to reload. The TypeScript server
is going to reboot from scratch. And
that should now recognize the new
package that we can import this format
function from here date fns. If it still
doesn't show up for you anyway, you can
always just import it manually here.
Import format from date fns. But that's
just the easier way to do it with
IntelliSense. Here we go. And we can
wrap the message timestamp in this
format function. That's the second
argument. This format function expects a
format string. So what we can pass in
here is the format we want this time to
be formatted in. In our case, that's
going to be uppercase h for hours and
then colon mm for minutes and hit save.
So that's going to show us the exact
time. Oops. And we need to restart the
development server. Here we go. That's
going to show us the exact time that the
message was sent at. Here we go. There
we go. It's 1559. This is in German
time. If you're American, uh, you know,
you're not used to my time format, but
it would say 340
59 p.m. here. So, the exact time the
message was sent. Beautiful, man. Now,
after this closing span, after one
closing div, that's where we're going to
open up with two more closing divs to go
and put a P tag here. This pet tag gets
a class name of text small text zinc 300
leading relaxed and break all. Here we
go. And then inside of this P tag, we're
going to dynamically insert the message
dot text. Here we go. To show whatever
the user has put in their message.
Beautiful. Hello world. We can now see
in the message. Now you might wonder
Josh, when I send a message here, it's
not real time, right? What did we
implement upstream for if I can't send a
message into the chat? It
doesn't work, Josh. Well, don't you
worry. The fix is super easy because we
did literally all the work. So, the only
thing we need to do now to make realtime
messages work is the following. Let's
use or real use real time hook that we
get from our lib realtime client right
here. Be careful. The use real time
needs to come from our own real-time
client and not from up-re directly.
Needs to come from this file, this
function right here. We want to use that
from lib realtime client right here
because then it's going to be
automatically typed. The other one would
theoretically work, but we already went
ahead and typed this one. So, let's use
this one. Great. Now, this takes a bunch
of options. For example, the channels.
which channel do we want to subscribe to
real-time events to? And that's going to
be our room ID because if you remember
on the back end when I show you the
route, which channel do we emit the real
time message to realtime channel room
ID, right? So, this is the channel we're
going to receive the event on on the
front end. The events we're going to
listen to are both the chat message and
the chat.estroy. So both events and
check how beautifully type- safe this is
and then on data will automatically be
called whenever we trigger this event
from our back end. in real time.
Actually, we can just dstructure the
event from the callback function here
and do a little check. If the event is
triple equal to chat dossage,
what do we want to happen? Basically, we
want to refetch the message history. The
message history is the single source of
truth for a chat. So in real time when
we detect a new message is sent let's
get the newest message history and
everything else is already taken care of
right. So the only thing we need to do
is to dstructure the refetch function
from usequery that's used to run this
query again whenever we want and simply
call it refetch whenever we get data
that is chat dossage in or use realtime
hook and that's it. It is actually as
easy as that, right? So, if I reload the
page, we get the history. And if I now
say my new message and hit enter, bam,
my new message. It's right here, man.
Beautiful. Now, the input field right
now is not cleared after we send a
message. That's really easy to implement
in or send message mutation. Let's just
say set input to an empty string after
sending a message. Let's save that.
Beautiful. Let's try again. Let's reload
my new message
again and then hit send. Bam. In real
time, my new message again. And now
everyone who is connected to this room
can see this. And just to validate, to
confirm, let's open a private tab. Paste
this link. And it seems like the room
just expired. Anyways, let's try this
again. Let's create a new room. Create
secure room. Here we go. Let's copy the
link. Paste that inside of a private tab
right here. And let's move that to the
side. This one to the other side. Here
we go. So, we have two chat instances
basically. Hello world. Hit enter. Bam.
We have it in both chats at the same
time. Hey, how are you? Hit enter. Bam.
And we can see it here on the other side
that anonymous hawk wxpo
whatever whatever wrote us a message.
And we can reload either of these chats.
Both are allowed to rejoin. But if I try
to join from anything else like for
example a incognito
chrome window for example I try to join
the same URL we get error roomfo.
Beautiful man. So nobody else is allowed
to join the room that these two are
connected to. They can reconnect. They
can chat in real time. Everything works
absolutely well in real time. This is
incredibly cool, man. Very, very cool
stuff. We now confirmed that this works.
So, let's move this to the side again.
Very, very nice work. Now, what should
happen in the other case where the event
is not chat message, but we're also
listening to chat.estroy.
So, let's quickly handle that. If the
event is equal to chat.destroy, well,
basically we want to kick the user to
the lobby, right? The database upstairs
redders handles all the expiration logic
anyways, right? The data will
automatically be be deleted after these
five minutes. So, we don't even need to
do anything here. The idea is just we
need to send users to the lobby. That's
the only thing we haven't handled yet.
And we can just use the use router hook
from React or from NexJS rather to do
that. So at the very top of our page,
let's say const router is equal to use
router and just import that hook from
next/navigation,
not next/ routouter next/navigation
because the next routouter import is for
the older Nex.js versions. I know
confusing, but that's just how they
decided to do it, I guess. And we're
going to say router.push
and we're going to push to slash with
the query pram of destroyed equals true.
So, this room is now gone. And that's
all we need to do. Let's save our page.
Beautiful. And let's actually handle
these errors while we're at it. Right?
We're already in the kind of designing
step of things. So, why don't we just
handle these errors while we're already
here? Let's go into our main page, the
lobby, to handle these errors. So, doing
this is very easy. First things first,
we need to read the error from the URL,
right? And there's a really convenient
hook we can use in Nex.js for that
called let's say const search params.
The hook is called use search params.
And this just gives us really easy
access to URL params. So the first thing
let's say const was destroyed. Was this
room destroyed? Is that why you're in
the lobby? We can base the error message
off of that is going to be equal to
search params.get get distur.
Here we go. And if that is triple equal
to true and this true needs to be a
string because a query is always a
string then in that case was destroyed
will evaluate to boolean. And while we
are already up here we can already grab
the const error. If anything else went
wrong let's get it from the URL search
params.get
error. Cool. And that's all the data we
need man. So, let's actually go ahead
and render this out. This is going to be
super easy. Basically, after our main
and after div, let's open up here and
we're going to do a conditional check.
If was destroyed, then we're going to
render out a div element. Oops, here we
go. A div element. This div will get a
class name, not an on click, man. What
am I doing here? We'll get a class name
of BG red 950
border border red 900 padding four and
text center. Inside of this div, let's
open this up. We're going to say in a p
tag room destroyed. And this p tag will
get a class name of text red 500, text
small, and font bold. Here we go. Right
after this P tag, one last P tag and
this one will get a class name of text
zinc 500 text XS for extra small and a
margin top of one. And inside of here,
let's say all messages were permanently
deleted. Period. Because this room was
just destroyed. Awesome. Okay, while
we're already doing the styling, let's
just handle the other cases because we
can just mark the entire section of was
destroyed. Hold shift, alt, and arrow
down once and twice. And just like that,
we copied the section three times.
Right, the first one can stay as it is.
For the second section, we're going to
check if the error is triple equal to
room not found because in that case we
are going to change the error message.
Instead of room destroyed, it should be
of course room not found. Here we go.
And then as the text we can say this
room may have expired or never existed.
Period. because this room is already
gone, you know. And then for the last
error message, we're going to check if
the error is triple equal to room full.
Here we go. That's how we let users
know, hey, two people are currently
chatting, you cannot join as well. So
instead of room destroyed, let's say
room full. And as the P tag, let's say
this room is at maximum capac capacity.
Period. Here we go, man. That's it.
Those are the error messages. And if we
now mock them to see if we did
everything right. If we go to the
homepage and do for example, let me zoom
in here so you can see better. For
example, uh question mark error is equal
to room full. And we zoom out again.
Here we go. Room full. This room is at
maximum capacity. We are displaying the
error message correctly. Beautiful,
dude. Now I think the only thing that's
left is if we create a secure room.
Well, there is right now no
self-destruct timer, right? So, let's do
that. The actual self-destruct mechanism
is already handled by Reddus as we
figured out, right? But we're not
actually showing that time on the page
here. And the way we're going to do that
is by initializing first off a state. In
that state, we're going to keep track of
the time remaining. We already did that
way back if you remember to mock this
state out here. Now when we load the
page, we need a way to get the time to
live from Reddus to know how much longer
this room has and then decrement that by
1 second every sec or by one every
second. Right? So first things first,
let's make a query to know how much
longer this room has to then set the
state based on that. Right? So let's say
const empty object, we'll worry about
the structuring later is equal to use
query inside of here. Let's put a query
key to identify our function against the
cache. We can just name this TTL time to
live. And then let's also put the room
ID in here. And then for the query
function, the actually important part.
This is going to be an asynchronous
error function right here. And now of
course we need a backend method we can
call here with our HTTP client, right?
So, let's go into our back end into our
main Eligia into the
router up here, the rooms router.
Beautiful. And right here, we're going
to create one new endpoint. This is
going to be super super simple. Let's
create a dot get endpoint right here
after the post endpoint. And let's name
this slashttl. And this will be nothing
else than a callback function. Now
before getting a time to live, we want
to do a check. Is the user who's
requesting this data, the time to live
for this room even allowed to do that?
Now technically this is not super
sensitive information, but I want to
teach you best practices and users who
are not allowed to join this room
shouldn't even be able to get the time
to live for this room. Right? So
therefore before this route we are going
to run dot use or off middleware. Here
we go. So this automatically runs before
this request but not before this one
because it's before that. Great. Now
inside of our TTL we can just dstructure
the O because we know the authentication
middleware will have run. And before we
get to the actual logic here let's also
enforce that in the query we pass a Z
dot object which is going to be or room
ID of type Z dot string right here. just
like we did with every other route in
our project. Great. Okay, let's open up
the logic. And this is going to be super
simple. Let's say const ttl is going to
be equal to and let's make this an
asynchronous function. So we can await
radis.ttl
the same command we used before. And we
simply want to fetch the TTL from meta
colon and then the o.room room ID. Here
we go. So we are getting the number how
much more um milliseconds does this have
to live and we can simply pass that back
to the front end. We can say return and
then as an object TTL and this can
either be if the TTL is larger than zero
the TTL or else zero right because
technically this can be lower than zero
maybe undefined something else I don't
know we just make sure it's a valid
integer here with this conditional
check. Beautiful. That's literally all
we need to do. Let's go back to our
front end. By the way, really cool
trick. If you want to switch between
files in VS Code or cursor, hold Ctrl
Alt and then press arrow right or arrow
left and you can just switch between
them. Really cool trick if you didn't
know that. Cool. Okay. So, in our TTL,
let's say right here, constress is equal
to await what we always did. client dot
room dot there's now a TTL method here
TTL dot get and of course in the query
we need to pass the room ID just like
with any other call and bam that's it we
can now return the res data beautiful
man let's dstructure the data from here
and name it TTL data here we go and then
let's put a use effect right below this
use query to synchronize the state with
the data from our back end. So inside of
a use effect, let's create that. This
takes a callback function and let's put
a dependency array that reruns the use
effect every time the TTL data changes.
Right? When this is fetched, this effect
should run. And the actual logic is
going to be really easy. If there is a
TTL data
TTL and that is not equal to undefined.
So basically it's any number right 01
whatever it might be in that case and
only then do we set the time remaining
to the TTL data TTL here we go and
that's it. So as soon as this is defined
in a valid integer, we set and
synchronize the state, right? So if I
reload the page, here we go. Every time
we reload, bam, we can see the state is
updated. And by default, let's set the
state to null because then our loading
state is also going to show here.
Beautiful. Now the only thing that's
left upon load, we get the right data
from Reddus. We know when this room is
going to self-destruct. Perfect. The
only thing that's left is actually
setting the state here every second.
Right? So this refreshes on the client
side as well. And nothing easier than
that. We can simply define a new use
effect right below this. And inside of
the effect, well, first off, let's pass
a dependency array to this. And this
will depend on two things. First is
going to be time remaining. Time
remaining. Here we go. And second is
going to be the router. And now let's
open this up. And I just want to check
something. Hello world. Is my internet
down? No, it's not. Okay, never mind. I
just wanted to check if my internet was
working because I had some problems
earlier. Anyways, so inside of this
effect, the first check we're going to
do if the time remaining is triple equal
to null or the time remaining is smaller
than zero. In that case, we're going to
return because that's not a case that we
want to handle because it's unlogical,
right? It means we initialized with a
empty or invalid time remaining, which
can happen before the data from our back
end is there. So to prevent any error,
we can just early return and everything
will be cool. If the time remaining is
exactly zero, in that case, we're going
to say router.push push and we're going
to push to the slash route to the lobby
with a error of question mark destroyed
equals true. So we're basically kicking
the user from the room as soon as the
time remaining has expired and we're
going to say return to stop any further
code execution beyond this point if this
is true. And now the marble the most
important part of this use effect it's
the interval that keeps updating this
value right here. Let's say const
interval is equal to set interval. Here
we go. And this takes a callback
function. And then as the second
argument, it takes the interval which
we're going to put to 1 second. So this
will run every second. And inside of
here, we're going to say set time
remaining. Here we go. And this takes a
callback function where we get access to
the previous value. And now if the
previous value is null so triple equal
to null or the previous value is smaller
or equal to one. In that case we're
going to say clear interval and pass it
the interval. So we're going to clean up
right it's done or work is done and
we're going to return zero from here.
Now in the other case we are safe to
deduct one from the um time right here.
So we can say return previous minus one
you know. So if we're at at like five
then this is going to be false. We're
going to say five minus one it's going
to be four three two and then eventually
this is going to be turn uh this is
going to be true and we're going to
return one and clear up automatically.
Beautiful. Now to just properly do this
as well, we're going to return the clear
interval from here as well to properly
clean up after itself if the component
ever dismounts so we don't get any
memory leak from the interval.
Beautiful. And if I now create another
room cuz that one has expired, it's
going to load the time from Reddus and
it's going to start counting down
automatically. Man, how cool is that? or
room is literally going to self-destruct
automatically based on Reddit's
expiration in exactly this time. If I
refresh the page, you can see it's
exactly what um the timer says. Every
message in here, hello or world will
automatically be deleted world after
this exact time for everyone because the
data is automatically erased from
Reddis. Very very good work, man. This
is awesome. And now the last thing is
this destroy now button. And this is
super easy to implement because we
literally already have implemented all
the logic to do this. So once again,
let's define the backend action first.
What should happen when a user wants to
delete a room immediately. Let's put it
after this get and let's say delete.
Here we go. This takes a call back
function, but I forgot before it takes
the callback function. It takes the
path. And let's do slash. So we can just
do you know room.delete. And that's it.
Beautiful. Now because this also runs
our o middleware, we can just dstructure
our o. You know the drill by now. And
now first things first, we can say await
radius.
Oo.
ID. So we're deleting the actual meta uh
not the metadata, the upstre realtime
history for this room. So all we need to
do technically is delete all data
associated with this room right now. So
that's going to be await reddis oops
reddis.dell the meta for the o room ID
right the meta data like who's connected
let's say await radis.deell
and we want to delete the messages
colon of room ID so the entire message
history and last thing await.
And we want to delete the history. And
we can say offroom room ID inside of
here as well. Oh, and you know what? I
apologize. This is actually not used in
our app anymore. I had this in my
original project. Um, but we already
store the message history in the
messages. So, my bad. Let's take this
out. Sorry. For these big projects,
sometimes small mistakes happen. I hope
you can forgive me. I just forgot that
was an unused property. with these three
data points, every information about the
room is already deleted. Now, um this is
just easy to write. I want to show you
one trick really quick to make this
faster because right now this code will
execute, it will wait, then this will
execute and then this will execute.
There's one really cool trick in
standard JavaScript we can do to make
this execute all at the same time
because no call here depends on one
another and that's a await promise.all.
I don't want to go too deep, but
basically if we remove the awaits here
using Ctrl D, I just marked them all and
then remove them and we move this up um
into the promise.all. Basically, all
Reddis calls will be executed at the
same time, essentially making our API a
lot faster, right? We could do the same
thing here down here for the Reddus
expirations. Um you can you can just do
that. It's really easy. Um and it makes
our back end so much faster. Just a cool
little trick I thought I should show
you. Anyways, that is almost it, right?
The only thing left is actually expect
for the query to be the same format is
always, which is a Z dobject
of type room ID Z dot string. So, our O
middleware is happy and knows what to
expect. And that's it. Just like that,
we can delete the room. And now to let
everyone know, hey, this room has been
deleted and kick them from the room,
let's just call await realtime channel
and we're going to emit to the channel
or do room id dot emit chat.destroy. So
we can emit the destruction event and
simply pass the data of is destroyed
true. Here we go. Just like that, we are
notifying all connected clients in real
time through our use real time hook way
down here because we're listening to
that event and pushing them out to the
lobby whenever somebody clicks this
button. Awesome, man. So, the absolutely
last thing we need to do is to hook up
this button to our front end, right? So,
the user on click can destroy a room
whenever they want. So, let's do that.
It's really easy. Let's define one last
mutation. Const empty object is equal to
use mutation.
Here we go. And as always, this takes a
mutation function. Cool. This mutation
function is an asynchronous error
function in which we can just do a wait
client.
[Music]
You can probably guess by now, delete.
And as the data we want to pass here,
that's going to be null. And then for
the query, we can pass in the room ID.
So our back end is happy. Beautiful.
Let's dstructure the mutate from here
and call it destroy
room. And that's literally it. We can
now use this function destroy room
whenever you click the destroy button
right here in our header. So to this
button, to the on click handler of the
destruction button, let's add the
destroy room. Here we go. and hit save.
So what should happen now? Let's go into
full screen here. Let me copy this. Open
in a new private tab and put these side
by side. Here we go. And now we are
writing. Hello, how are you? Bam. Write
that to the other guy. We see the
message in real time. Beautiful. And if
one of them now clicks destroy. Bam.
Here we go. We are destroying the room
and then sending back both people to the
lobby. If everything works correctly,
which it doesn't seem to be, what is
happening? Let's go into the network
tab. Destroy now. Uh, what is happening?
405
request response. What is happening?
All right, let's debug this together in
real time. Do we have any error on the
terminal 405?
Interesting. Aha. And after like 10
seconds of thinking what the issue could
be, um there's one config we need to do
in our Elia that's way down here. Um
right now we only accept get and post
requests to our endpoint and we also
need to um support delete. Right? So all
we need to do is say export condeal to
app.fetch and that should be it. Cool.
All right. So let's open these up side
by side again. Uh here we go. And now
let's check. Do the messages work? Of
course they do. I just want to demo this
to you. And if one of the guys now
clicks destroy, bam, let's hit destroy.
Both rooms are automatically or not both
rooms are destroyed but the room is
destroyed and both people are
automatically kicked from the room and
the entire data is automatically wiped
from Reddus as we can see here. Ah, and
there's one little bug and I know
exactly why this is happening. As you
can see one data point is still
happening or still not being deleted and
that is the is destroy true event that
we are emitting from up real time. So
the problem here is that if we take a
look at the code execution order we are
deleting the metadata for the room and
the messages and so on and then we are
emitting. So let's change the order
holding alt and the arrow keys I can
move this up. So we emit first and then
we delete from radius in that order.
Beautiful. So that should now work.
Let's open back up the other browser.
Here we go. Let's delete this manually.
And let's try this from scratch again.
Let's create a room. Here we go. That's
going to put us into the room. We can
see in our database. The room is now
created with metadata. And here we go.
An expiration date. Beautiful. Very,
very nice. Hey, this is me editing Josh
real quick. So, I was done with this
video, but I noticed one quick um
mistake I made in the video. And so,
when we run bun run build or npm run
build, this is the command that the
server will run when we deploy our
application on Verscell or anywhere
else. And this actually right now gives
us a error. Now, this error is really
easy to fix, but I forgot it in the
original video, so I wanted to add it
really quick. So this error we can
easily fix in our page right here in our
main uh page not the room page. It is
because of these use search params
right? So the error is use search params
should be wrapped in a suspense
boundary. So what does this mean? How
can we fix it? Well the fix is really
easy. We can say const page. So we're
going to declare a separate component
and then simply render this component
and wrap it in suspense. So let's call
the original home component we had for
example function lobby because it's the
lobby of our app and then simply return
the lobby
here from our page component and wrap
that in a react suspense just like this.
This suspense is a builtin react
feature. We can just import from React
and then we can say export default page
or even move this to the very bottom of
the file by convention. And if we now
run the build step again, here we go.
Then our nextJS, we'll see that hey this
lobby this client side component is now
wrapped in a suspense and everything is
cool with using the search params. We
can now copy this link, paste it in our
other browser so this guy can join. Here
we go. We're now in a room. We can write
with each other. Hey, what's up? And the
other person is going to receive that in
real time. Beautiful.
Yo yo gang, what's up? And when one of
them clicks the destroy now button, the
room is immediately destroyed. Both
people are kicked to the lobby and all
data is irreversibly deleted from our
database. Everything is wiped. the
entire history, metadata, everything.
Cool, man. That is really, really nice.
We just built something amazing
together. I really hope you enjoyed
today's build. I had so much fun while
making this, and I hope you learned a
lot while doing this, especially around
real time, how to implement real-time
features securely. And I really hope you
enjoyed building this, learning about
the NexJS proxy, about routing patterns,
about dynamic params, about um Eligia
and modern NexJS backends, type safety.
There was so much in this video. I
really hope you enjoyed building
together with me. If you did, just like
the video, man. That's it. Like just
just like the video. Maybe write a
comment would be really cool. And that's
all I'm asking. Thanks so much for
watching. I really hope you enjoyed the
build. And then I'll see you in the next
video. Until then, have a good one and
bye-bye.
lets build a complete real-time chat with the newest nextjs 16, redis, tailwind, typescript and elysia :] -- links code: https://github.com/joschan21/nextjs16_realtime_chat twitter: https://x.com/joshtriedcoding github: https://github.com/joschan21