The Creation Journey of “GPT-Creator”: A TypeScript and Express App

Mehmet Acar
10 min readDec 25, 2023

--

Once upon a time in the bustling world of software development, a visionary idea took root: the creation of “gpt-creator” a versatile app engineered using TypeScript and Express. This is the tale of its journey from inception to realization.

As part of setting up the project, it’s essential to have access to the source code. For convenience and collaboration, “GPT-Creator” is hosted on GitHub, allowing users to clone, contribute, or modify the project as per their requirements. You can access the full source code and detailed setup instructions at the following GitHub repository:

https://github.com/macaris64/gpt-creator

Chapter 1: The Genesis

In the digital realm, an idea sparked to life: the “gpt-creator” Its first breath was drawn with the initiation of a project environment.

# Initialize Node.js environment
nvm install 20.9.0
nvm use 20.9.0

# Create a directory for the project
mkdir gpt-creator
cd gpt-creator

# Initialize the project
npm init -y

Chapter 2: Laying the Foundations

With the project space ready, the foundational blocks were placed, installing TypeScript, Express, and other necessary packages.

npm i --save-dev typescript @types/express nodemon ts-node @types/sequelize
npm i express pg pg-hstore sequelize dotenv openai

Chapter 3: Sketching the Blueprint

The next step was to sketch the app’s blueprint, creating the structure for code and configurations.

  • Create a ./.nvmrc file on the root of the project with 20.9.0 as its content to lock the Node.js version.
  • Set up the src folder and src/index.ts
  • Set up ./tsconfig.json and ./package.json in the root of the project.

./package.json

{
"name": "gpt-creator",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"serve": "node build/index.js"
},
"author": "",
"license": "ISC",
...
}

./tsconfig.json

{
"compilerOptions": {
"target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"typeRoots": ["./node_modules/@types", "/.types"], /* Specify multiple folders that act like './node_modules/@types'. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

In addition to setting up the basic structure, configuring environment variables is a crucial step. This is where the .env file comes into play. It stores sensitive information and configurations that shouldn't be hard-coded into the source code or shared publicly. For "GPT-Creator", the ./.env file includes settings for PostgreSQL and the OpenAI API key:

# PostgreSQL Configuration
POSTGRES_DB=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_HOST=postgres
POSTGRES_PORT=5432

# OpenAI Configuration
OPENAI_API_KEY=<Your_OpenAI_API_Key>

It’s important to replace <Your_OpenAI_API_Key> with the actual API key obtained from OpenAI.

Chapter 4: Crafting the Heartbeat

The journey continued with the creation of src/index.ts, the heartbeat of our application. Here, Express was brought to life, establishing the server's foundation.

src/index.ts

import express from 'express';
const app = express();
const port = 3008; // The port on which the server will run
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});

This simple server setup marked the first interaction between our app and the outside world, echoing a ‘Hello World!’ to any visitor.

How to Run the Project & What to Expect on the Landing Page

After setting up src/index.ts, running the project is straightforward. Execute the command npm start, and the server will come to life, listening on port 3008. When you navigate to http://127.0.0.1:3008/, you will be greeted with the simple yet welcoming message: 'Hello World!'. This serves as the landing page, confirming that the server is up and running.

Chapter 5: Mapping the Routes

The construction of src/routes/gpt.route.ts was the next phase. This file served as a navigator, outlining the paths our app would respond to.

src/routes/gpt.route.ts

// src/routes/gpt.route.ts
import express from 'express';
const router = express.Router();

import * as gptController from '../controllers/gpt.controller';

router.post('/gpt', gptController.createGpt);
router.post('/send', gptController.send);

export default router;

These routes would later connect to the controllers, bridging user requests to appropriate actions.

Details of the Endpoints

  1. POST /gpt: This endpoint is used for creating new GPT instances. It expects details like the name and system message for the GPT instance in the request body. Once a GPT instance is created successfully, it returns the details of the instance.
  2. POST /send: This endpoint is designed for sending messages to a specific GPT instance. It requires the GPT instance ID and the message to be sent in the request body. The endpoint then processes the message using the specified GPT instance and returns the GPT’s response.

These endpoints are the core functionalities of “GPT-Creator,” allowing users to interact with the GPT models in a structured and controlled manner.

Chapter 6: Instilling Intelligence

With routes in place, focus shifted to src/controllers/gpt.controller.ts. Here, the logic for handling GPT instance creation and message communication was encapsulated.

src/controllers/gpt.controller.ts

// src/controllers/gpt.controller.ts
import { Request, Response } from 'express';

export const createGpt = async (req: Request, res: Response) => {
// Placeholder for GPT creation logic
res.send('GPT creation endpoint');
}

export const send = async (req: Request, res: Response) => {
// Logic to send a message to a GPT instance
res.send('GPT send endpoint');
}

These functions were just placeholders, waiting to be imbued with real functionality.

Chapter 7: Weaving the Safety Net

No app is complete without a robust error handling mechanism. The creation of src/utils/errors.ts and src/middlewares/error.middleware.ts was like weaving a safety net, ensuring that the app could handle unexpected situations gracefully.

src/utils/errors.ts

export class APIError extends Error {
public readonly status: number;
public readonly isOperational: boolean;

constructor(status: number, message: string, isOperational: boolean = true) {
super(message);
this.status = status;
this.isOperational = isOperational;
Object.setPrototypeOf(this, new.target.prototype);
}
}

src/middlewares/error.middleware.ts

import { Request, Response, NextFunction } from 'express';
import { APIError } from "../utils/errors";

export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof APIError) {
console.log('API Error:', err);
res.status(err.status).json({ message: err.message });
return;
}

res.status(500).json({ message: 'Internal Server Error' });
};

Chapter 8: Sailing the Docker Seas

The tale took a turn towards the modern era of deployment — dockerization. Crafting a Dockerfile and a docker-compose.yml was akin to building a ship, ready to sail the seas of cloud-based environments.

Dockerfile

FROM node:20.9.0
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3008
CMD ["npm", "start"]

docker-compose.yml

version: '3'
services:
app:
build: .
ports:
- "3008:3008"
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
env_file:
- .env
postgres:
image: "postgres:latest"
volumes:
- ./postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
ports:
- "5432:5432"
env_file:
- .env
volumes:
postgres_data:

These configurations ensured that “gpt-creator” could be easily deployed and scaled, regardless of the environment.

docker-compose run --build

This command reads the Dockerfile and docker-compose.yml, building the necessary image for our app and the PostgreSQL service and starting the services defined in our docker-compose.yml file. It includes our "GPT-Creator" app and the PostgreSQL database, linking them as specified.

With the containers running, you can access the “GPT-Creator” application by navigating to http://127.0.0.1:3008/ in your web browser. You should see the 'Hello World!' message, indicating that the app is running successfully.

Now that the app is up, you can interact with it using the endpoints /gpt and /send as described previously. These can be accessed via any API testing tool like Postman or even through command-line tools like curl.

When you’re done, you can stop the Docker containers by executing:

docker-compose down

Chapter 9: Taming the Database Beast

As our tale of “GPT-Creator” progressed, it was time to confront the formidable challenge of data management. The integration of a PostgreSQL database was crucial for handling the data intricacies of our application.

To establish this connection, the src/utils/sequelize.ts file was created. This file was like the negotiator, ensuring smooth communication between the app and the database.

src/utils/sequelize.ts

import { Sequelize } from 'sequelize';

const sequelize = new Sequelize(
process.env.POSTGRES_DB!,
process.env.POSTGRES_USER!,
process.env.POSTGRES_PASSWORD!, {
host: process.env.POSTGRES_HOST,
port: parseInt(process.env.POSTGRES_PORT || '5432'),
dialect: 'postgres',
logging: false // Set to console.log for seeing SQL logs
});

export default sequelize;

With Sequelize, the app was now empowered to interact with the PostgreSQL database, paving the way for dynamic data operations.

Chapter 10: Constructing the Data Models

The next step was to define the data models. This was akin to creating a blueprint for how data would be structured within the database. The creation of src/models/gpt.model.ts served this purpose.

import { DataTypes, Model, Sequelize } from 'sequelize';

export class GPT extends Model {
public id!: number;
public name!: string;
public systemMessage!: string;
}

export function initGptModel(sequelize: Sequelize): void {
GPT.init({
id: {
type: DataTypes.INTEGER.UNSIGNED,
autoIncrement: true,
primaryKey: true,
},
name: {
type: new DataTypes.STRING(128),
allowNull: false,
},
systemMessage: {
type: new DataTypes.TEXT,
allowNull: false,
},
}, {
tableName: 'gpts',
sequelize,
timestamps: true,
});
}

The GPT class and its initialization function laid the foundation for storing and retrieving GPT instances in the database.

Chapter 10: Enlivening the Models

After defining the models, it was time to bring them to life. This was done by integrating the model initialization in the src/index.ts file, completing the connection between the app's core functionality and the database.

import dotenv from 'dotenv';
import express from 'express';
import gptRouter from './routes/gpt.route';
import {errorHandler} from "./middlewares/error.middleware";

import sequelize from "./utils/sequelize";
import {initGptModel} from "./models/gpt.model";

initGptModel(sequelize);

const app = express();
const port = 3008; // You can choose any port

dotenv.config();
app.use(express.json());
app.use('/api', gptRouter);

app.get('/', (req, res) => {
res.send('Hello World!');
});

app.use(errorHandler);

sequelize.authenticate()
.then(() => {
console.log('Connection has been established successfully.');
return sequelize.query('CREATE TABLE IF NOT EXISTS test_table (id SERIAL PRIMARY KEY, test_column VARCHAR(100));');
})
.then(() => {
console.log('Test table created');
return sequelize.query('DROP TABLE IF EXISTS test_table;')
})
.catch(err => console.error('Unable to connect to the database:', err));


sequelize.sync({ force: false }).then(() => {
console.log('Database & tables created!');
app.listen(port, () => {
console.log(`Server is running port ${port}`);
});
})

This integration ensured that when the app was started, the database connection would be established and the models would be ready for use.

Chapter 11: The GPT Instance Manager

As our story nears its climax, the creation of the “GPT Instance Manager” marks a pivotal chapter. This manager was the architect behind the creation, updating, and overall management of GPT instances. It acted as the liaison between the app and the OpenAI’s GPT models.

import { OpenAI } from 'openai';
import { GPT } from '../models/gpt.model';
import { APIError } from '../utils/errors';

class GPTInstanceManager {
private openai: OpenAI;

constructor() {
this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
}

async createGPTInstance(name: string, systemMessage: string): Promise<GPT> {
return GPT.create({ name, systemMessage });
}

async sendMessageToGpt(id: number, message: string): Promise<any> {
const gptInstance = await GPT.findByPk(id);
if (!gptInstance) {
throw new APIError(404, 'GPT instance not found');
}

return this.openai.chat.completions.create({
messages: [
{ role: 'system', content: gptInstance.systemMessage },
{ role: 'user', content: message }
],
model: 'gpt-4',
response_format: { type: 'text' },
max_tokens: 3000
});
}

async getGPTInstanceByName(name: string): Promise<GPT | null> {
return GPT.findOne({ where: { name } });
}
}

export default GPTInstanceManager;

This manager was a crucial component, enabling our app to dynamically interact with AI, blending the realms of database management and artificial intelligence.

Requirement of OpenAI API Key

For the GPT Instance Manager to function, users must have an OpenAI account to obtain an API key. This key is crucial for the app to interact with OpenAI’s GPT models. Without this key, the app won’t be able to create or manage GPT instances or process any AI-related tasks. Users should sign up for an OpenAI account and generate an API key, which then needs to be placed in the .env file as shown earlier.

Chapter 11: Completing the Circle

With the GPT Instance Manager in place, the final step was to implement the endpoints. This involved updating the src/controllers/gpt.controller.ts file to use the GPT Instance Manager for creating and communicating with GPT instances.

import GPTInstanceManager from '../managers/gpt';

export const createGpt = async (req: Request, res: Response) => {
const gptManager = new GPTInstanceManager();
const { name, systemMessage } = req.body;

let gptInstance = await gptManager.createGPTInstance(name, systemMessage);
res.status(200).json({ gptInstance });
}

export const send = async (req: Request, res: Response) => {
const gptManager = new GPTInstanceManager();
const { id, message } = req.body;

const response = await gptManager.sendMessageToGpt(parseInt(id), message);
res.status(200).json({ message: response });
}

This completion of the endpoints marked the full realization of “GPT-Creator’s” capabilities, offering endpoints for creating GPT instances and communicating with them.

Example Request for /gpt

POST http://127.0.0.1:3008/gpt
Content-Type: application/json

{
"name": "Albert Einstein",
"systemMessage": "Be kind and respectful"
}

Example Response

{
"gptInstance": {
"id": 15,
"name": "Albert Einstein",
"systemMessage": "Be kind and respectful",
"updatedAt": "2023-12-25T16:28:34.764Z",
"createdAt": "2023-12-25T16:28:34.764Z"
}
}

Example Request for /send

POST http://127.0.0.1:3008/send
Content-Type: application/json

{
"id": 15,
"message": "Hello"
}

Example Response for /send

{
"message": {
...
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I assist you today?"
},
"logprobs": null,
"finish_reason": "stop"
}
],
...
}
}

Epilogue: A Journey Remembered — The Versatility of GPT-Creator

As we draw the curtains on the story of “GPT-Creator,” it’s essential to reflect on the versatile applications of this innovative tool. Designed with the power of OpenAI’s GPT models and the robustness of TypeScript and Express, “GPT-Creator” opens a realm of possibilities:

Creating Custom GPTs: At its core, “GPT-Creator” excels in creating custom GPT instances. Users can define specific parameters and system messages, tailoring the AI’s behavior and responses to suit diverse applications, from personalized chatbots to specialized AI-driven analysis tools.

Crafting Chat Threads: The app is adept at handling dynamic chat threads. Whether it’s for customer service, interactive storytelling, or online forums, “GPT-Creator” can manage and respond to conversations, providing intelligent, context-aware interactions.

Building Assistants: “GPT-Creator” can be the backbone of AI assistants, capable of understanding and responding to various queries. It can be integrated into virtual assistants for businesses, personal productivity tools, or even as part of a larger AI ecosystem.

Enabling Image Generation: Though primarily focused on text processing, “GPT-Creator” can be extended to work with image generation models. This feature can revolutionize fields like graphic design, content creation, and visual media, where AI-generated images can provide substantial value.

And Beyond: The application’s use cases extend far beyond these examples. From generating creative writing and poetry to analyzing large datasets for insights, “GPT-Creator” stands as a bridge between human creativity and AI capability.

In my next articles, I will talk about custom gpts and their uses.

--

--