In this article we will look at an example project combining Next.js,Socket.IO, Express and react. Thus we create a chat application.
1. Bubbly - Full stack chat application
This is a Full Stack chat application created w/ Next.js, Socket.IO, Express, React and TypeScript.
Live demo deployed on Heroku is here.
Features
Creating chat rooms for real-time chat
Joining conversations by using themed rooms
JWT based anonymous authentication
Users can set their username and conversation bubble color
Sending images/gifs
Emoji picker
"User is typing" notifications
Dark theme
Automatically linkifying urls, emails etc
Invite/share buttons
Sound notification when the window is not focused
"Back to bottom" button to scroll down automatically
Stack
- Framework: Next.js (w/ Custom Express Server Integration)
- Real-Time Engine: Socket.IO
- Authentication: JSON Web Token
- UI Components: Material-UI
- Styling: styled-components
- Forms: Formik
- Form Validations: Yup
- Illustrations: unDraw
- Linting: ESLint
- Code Formatting: Prettier
Development
To run it in development mode:
Step 1: npm install
Step 2: npm run dev:custom
Production
To create a production build ready to deploy, you need to set a JWT_SECRET_KEY
in .env
file first.
After that, run:
Step 1: npm run build
Step 2: npm start
Sample Code
index.ts
import 'dotenv/config';
import express, { Request, Response } from 'express';
import next from 'next';
import http from 'http';
import { CustomSocketIoServer } from './socket/CustomSocketIoServer';
import { convertMBToByte } from './shared/shared.utils';
import { SocketUser } from './users/SocketUser';
import socketHandler from './socket/socket.handler';
import usersHandler from './users/users.handler';
import roomsHandler from './rooms/rooms.handler';
import chatHandler from './chat/chat.handler';
import { registerHelmet } from './security/security.utils';
const dev = process.env.NODE_ENV !== 'production';
const nextApp = next({ dev });
const nextHandler = nextApp.getRequestHandler();
nextApp.prepare().then(() => {
const expressApp = express();
registerHelmet(expressApp);
const maxMessageSizeInMB = 2;
const httpServer = http.createServer(expressApp);
const io = new CustomSocketIoServer(httpServer, {
// Max size for a message
// TODO: We need a way to handle exceptions
// those caused by this option on the client side.
maxHttpBufferSize: convertMBToByte(maxMessageSizeInMB),
});
io.use((socket, nextFn) => {
const user = SocketUser.findOrCreateSocketUser(io, socket);
io.addSocketUser(user);
// eslint-disable-next-line no-param-reassign
socket.user = user;
nextFn();
});
io.on('connection', (socket) => {
socketHandler(io, socket);
usersHandler(io, socket);
roomsHandler(io, socket);
chatHandler(io, socket);
});
expressApp.all('*', (req: Request, res: Response) => nextHandler(req, res));
const port = process.env.PORT ?? 3000;
httpServer.listen(port, () => {
// eslint-disable-next-line no-console
console.log(<code>> Ready on http://localhost:${port}</code>);
});
});
ChatMessage.ts
import { nanoid } from 'nanoid';
import { ID, Maybe } from '@shared/SharedTypes';
import socketIo from 'socket.io';
import { SocketUser } from '../users/SocketUser';
import { isImageFile } from './chat.utils';
import { trimSpaces } from '../shared/shared.utils';
export interface ChatMessageInput {
body: Maybe<string>;
file: Maybe<Buffer | string>;
}
export class ChatMessage {
id: ID;
author: SocketUser;
body: Maybe<string>;
timestamp: number;
file: Maybe<Buffer>;
// We made this "private" to prevent this to be called from outside.
// We shouldn't use this constructor directly.
// We should use "createChatMessage" static method to create new messages.
private constructor(args: {
socket: socketIo.Socket;
body: Maybe<string>;
file: Maybe<Buffer>;
}) {
const { body, file, socket } = args;
this.id = nanoid();
this.author = socket.user;
this.body = body;
this.timestamp = Date.now();
this.file = file;
}
// Because that we can't make the constructor async,
// we create ChatMessage instances with this static method.
static createChatMessage = async (
args: ChatMessageInput & { socket: socketIo.Socket },
): Promise<ChatMessage> => {
const { socket, body, file } = args;
const trimmedBody = trimSpaces(body ?? '');
if (!trimmedBody && !file) {
throw new Error('At least a message body or a file is required.');
}
// "file" can be a Buffer or base64 string.
// base64 string is used for react-native app here.
let inputFile = null;
if (file instanceof Buffer) {
inputFile = file;
} else if (typeof file === 'string') {
inputFile = Buffer.from(file, 'base64');
}
if (inputFile) {
const isImage = await isImageFile(inputFile);
if (!isImage) {
throw new Error('File is not an image.');
}
}
const message = new ChatMessage({
socket,
body: trimmedBody,
file: inputFile,
});
return message;
};
}
Reference