An open API service indexing awesome lists of open source software.

https://github.com/serbanuntu/messenger

A lightweight instant messaging application built with Express.js
https://github.com/serbanuntu/messenger

express jwt postgresql react socket-io

Last synced: 2 months ago
JSON representation

A lightweight instant messaging application built with Express.js

Awesome Lists containing this project

README

        

# Messenger

A lightweight instant messaging application built with Express.js

![Banner](/public/banner.jpg)

## About

This chat application allows users to create conversations and send messages to each other in real time.

**Features**:
- One-to-one conversations
- Group conversations (3 or more people)
- Real-time updates when receiving messages or being added to conversations
- Optimistic UI updates to minimize waiting for server operations
- Website rendered as a SPA allowing navigation to different pages without a full browser refresh
- Fully responsive UI (works on both mobile and desktop)
- Account system

## Technologies

![Logos of used technologies](/public/stack.jpg)

### Frontend

- **React**: A framework for manipulating the DOM declaratively.
- **v0 by Vercel**: An AI tool used to build user interfaces from prompts. It outputs Next.js code by default. I used it to build the UI very quickly.
- **shadcn/ui**: A registry of unstyled reusable components. Used for more complex components such as toast notifications. Also used by v0.
- **TailwindCSS**: A CSS library that includes utility classes to allow styling of components direclty in the HTML.
- **Webpack**: A build tool used to bundle all the React code into a single minified, optimized and compressed plain Javascript file ready for production.

### Backend

- **Node.js**: A runtime for Javascript on the server (note that `ts-node` is used to run the Typescript code which is not natively supported by Node).
- **Express.js**: A framework for handling server-side routing, browser requests, middleware and the responses sent back to the browser.
- **Socket.io**: A library for handling Websocket connections for realtime updates to the messages and conversations.
- **JSON Web Tokens**: A stateless way of managing user authentication with symmetric encryption.
- **node-postgres**: A library for sending SQL queries to the Postgres database from the Node server.

### Deployment

- **Neon**: A Postgres serverless runtime built with Rust. I highly recommend their [Postgres tutorial](https://neon.tech/postgresql/tutorial).
- **Google Cloud Run**: A cloud service that manages and runs the server instance.
- **Docker**: A platform used to define a reproducible configuration for the server build, so it can be hosted anywhere.

## What I Learned

### Authentication with JWT

For this project I rolled out my own authentication to avoid importing a heavy library and to learn more about how to keep sensitive information secure.

When the user logs in, the server will generate a new token using the `JWT_SECRET` environment variable, which acts as the encryption / decryption key.

The token stores the user id in its payload, so it can be used to fetch the user that made the request if needed.

```ts
// auth.ts
export const getCurrentUser = async (req: Request, res: Response) => {
const token = req.cookies.messenger_jwt
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { user_id: number };
const user = await getUserById(decoded.user_id)
res.status(200).json(user)
} catch (err) {
res.status(401).json({ error: (err as Error).message })
}
}
```

### Compression

I fell into a bit of a compression rabbit hole while trying to get that 100 Performance Lighthouse score.

I learned about the 4 main types of compression used by browsers (gzip, deflate, brotli and zstandard) and decided to also serve a Brotli-encoded variant of my bundled JS.

```ts
// webpack.config.ts
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
level: 11
},
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false,
}),
```

The compression middleware in the `compression.ts` file will look into the `Accept-Encoding` header from the request to see if the browser supports Brotli.

This simple change reduces the size of the JS file sent over the network from 510 KB to **120 KB**.

### Real-time Communication

On the server, I store the `Socket` objects for each connected user in a *hashmap*, so I can only target the necessary users that should be notified.

```ts
// websockets.ts
const sockets: Map = new Map()

export const emitMessage = async (message: Message) => {
const targets = await getUsersInConversation(message.conversation_id, message.author_id) as { user_id: number }[]
targets.forEach(u => {
const socket = sockets.get(u.user_id)
if (socket) {
socket.emit('message', message)
console.log(`Message from user ${message.author_id} received by ${u.user_id}`)
}
})
}
```

*Note: It might be possible to use channels and subscriptions to achieve a cleaner implementation*.

### Deployment

Since this application requires long-lived connections between the server and client for realtime updates, serverless providers such as Vercel weren't an option.

I initially tried to upload the Github repo directly to Google Cloud Run but I didn't succeed.

To simplify the configuration, I got my server up and running using Docker before uploading it to Cloud Run again.

## Further development

- Light theme
- Image sharing
- Message reactions
- Enhanced onboarding and usage instructions
- Message encryption