Introduction
In the rapidly evolving landscape of software development, generative AI has become an indispensable tool for JavaScript and TypeScript developers. Whether you're building React components, crafting Node.js backends, or writing utility functions, the right prompt can save hours of boilerplate coding. But not all prompts are created equal — a vague request might yield unusable code, while a well-structured prompt can produce production-ready solutions.
This guide curates 15 expert-level prompts organized by difficulty — Basic, Advanced, and Expert — with real-world examples and practical tips. By the end, you'll know exactly how to phrase your requests to get the best possible code from AI assistants like GPT-4, Claude 3.5, or Gemini. Each prompt includes a task description, the exact prompt text, and a concrete code example to illustrate the output.
Why Structured Prompts Matter for Code Generation
Modern LLMs excel at pattern matching. When you provide clear context, constraints, and expected output format, the generated code is significantly more reliable. According to Anthropic's prompt engineering guide (2024), specifying the programming language, framework version, and even the coding style (e.g., functional vs. OOP) can improve accuracy by up to 40%. For TypeScript, including type definitions in the prompt reduces hallucinated APIs.
Basic Prompts: Getting Started with Simple Code
1. Generate a Debounce Function
Task: Create a generic debounce utility in TypeScript.
Prompt:
Write a debounce function in TypeScript with generic type support. It should accept a callback function and a delay in milliseconds. Return a debounced version that delays invocation. Include proper TypeScript types and JSDoc comments.
Example Result:
/**
* Creates a debounced function that delays invoking `fn` until after `delay` milliseconds
* have elapsed since the last time the debounced function was invoked.
*/
export function debounce<T extends (...args: unknown[]) => unknown>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
fn(...args);
timeoutId = null;
}, delay);
};
}
2. Generate a Simple React Hook: useLocalStorage
Task: Create a hook that syncs state with localStorage.
Prompt:
Create a React custom hook called useLocalStorage that accepts a key and an initial value. It should return a stateful value and a setter function, persisting changes to localStorage. Use TypeScript generics for type safety. Handle JSON serialization errors gracefully.
Example Result:
import { useState, useEffect, useCallback } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key “${key}”:`, error);
return initialValue;
}
});
const setValue = useCallback((value: T | ((prev: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key “${key}”:`, error);
}
}, [key, storedValue]);
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue !== null) {
setStoredValue(JSON.parse(e.newValue) as T);
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);
return [storedValue, setValue];
}
3. Generate a Simple Express.js Route
Task: Create a RESTful route for a user resource.
Prompt:
Write an Express.js route handler for a GET /users/:id endpoint. It should fetch a user from a PostgreSQL database using the pg library, return 200 with JSON, or 404 if not found. Use async/await and proper error handling.
Example Result:
const express = require('express');
const { Pool } = require('pg');
const router = express.Router();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
router.get('/users/:id', async (req, res) => {
const { id } = req.params;
// Validate ID is a number
const userId = parseInt(id, 10);
if (isNaN(userId)) {
return res.status(400).json({ error: 'Invalid user ID' });
}
try {
const result = await pool.query('SELECT id, name, email FROM users WHERE id = $1', [userId]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error('Database error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;
Advanced Prompts: Composing Complex Solutions
4. Generate a TypeScript Generic API Client
Task: Build a type-safe HTTP client wrapper around fetch.
Prompt:
Create a TypeScript generic API client class that wraps fetch. It should support GET, POST, PUT, DELETE methods, accept generic request and response types, handle errors uniformly, and include a request interceptor for adding auth headers. Use the builder pattern for configuration.
Example Result:
type HttpMethod = 'GET'
| 'POST' | 'PUT' | 'DELETE';
interface ApiClientConfig {
baseUrl: string;
headers?: Record<string, string>;
interceptors?: {
request?: (config: RequestInit) => RequestInit;
response?: <T>(response: Response) => Promise<T>;
};
}
export class ApiClient {
private config: ApiClientConfig;
constructor(config: ApiClientConfig) {
this.config = config;
}
private async request<TReq, TRes>(
method: HttpMethod,
path: string,
body?: TReq
): Promise<TRes> {
const url = `${this.config.baseUrl}${path}`;
let requestInit: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: body ? JSON.stringify(body) : undefined,
};
// Apply request interceptor
if (this.config.interceptors?.request) {
requestInit = this.config.interceptors.request(requestInit);
}
try {
const response = await fetch(url, requestInit);
// Apply response interceptor
if (this.config.interceptors?.response) {
return await this.config.interceptors.response<TRes>(response);
}
if (!response.ok) {
const errorBody = await response.text();
throw new ApiError(response.status, errorBody);
}
return (await response.json()) as TRes;
} catch (error) {
if (error instanceof ApiError) throw error;
throw new ApiError(0, `Network error: ${(error as Error).message}`);
}
}
get<TRes>(path: string): Promise<TRes> {
return this.request<never, TRes>('GET', path);
}
post<TReq, TRes>(path: string, body: TReq): Promise<TRes> {
return this.request<TReq, TRes>('POST', path, body);
}
put<TReq, TRes>(path: string, body: TReq): Promise<TRes> {
return this.request<TReq, TRes>('PUT', path, body);
}
delete<TRes>(path: string): Promise<TRes> {
return this.request<never, TRes>('DELETE', path);
}
}
export class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = 'ApiError';
}
}
5. Generate a React Form with Validation (Formik + Yup)
Task: Create a user registration form with validation.
Prompt:
Build a React registration form component using Formik and Yup. Include fields: name (string, required), email (valid email), password (min 8 chars, at least one number and one special char). Show inline validation errors. Use TypeScript.
Example Result:
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
interface RegistrationFormValues {
name: string;
email: string;
password: string;
}
const validationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
email: Yup.string().email('Invalid email format').required('Email is required'),
password: Yup.string()
.min(8, 'Password must be at least 8 characters')
.matches(/[0-9]/, 'Password must contain at least one number')
.matches(/[!@#$%^&*]/, 'Password must contain at least one special character')
.required('Password is required'),
});
const RegistrationForm: React.FC = () => {
const initialValues: RegistrationFormValues = { name: '', email: '', password: '' };
const handleSubmit = (values: RegistrationFormValues, { setSubmitting }: { setSubmitting: (isSubmitting: boolean) => void }) => {
// Simulate API call
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 1000);
};
return (
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded shadow">
<h2 className="text-2xl font-bold mb-4">Register</h2>
<Formik initialValues={initialValues} validationSchema={validationSchema} onSubmit={handleSubmit}>
{({ isSubmitting }) => (
<Form>
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium text-gray-700">Name</label>
<Field type="text" name="name" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
<ErrorMessage name="name" component="div" className="text-red-500 text-sm mt-1" />
</div>
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
<Field type="email" name="email" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
<ErrorMessage name="email" component="div" className="text-red-500 text-sm mt-1" />
</div>
<div className="mb-4">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
<Field type="password" name="password" className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2" />
<ErrorMessage name="password" component="div" className="text-red-500 text-sm mt-1" />
</div>
<button type="submit" disabled={isSubmitting} className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 disabled:opacity-50">
{isSubmitting ? 'Submitting...' : 'Register'}
</button>
</Form>
)}
</Formik>
</div>
);
};
export default RegistrationForm;
6. Generate a Node.js Rate Limiter Middleware
Task: Implement an in-memory rate limiter for Express.
Prompt:
Write an Express middleware for rate limiting using an in-memory store. Allow configuration of windowMs (time window in milliseconds) and maxRequests (max requests per window). Return 429 with a Retry-After header when limit exceeded. Use TypeScript.
Example Result:
import { Request, Response, NextFunction } from 'express';
interface RateLimitEntry {
count: number;
resetTime: number;
}
interface RateLimitConfig {
windowMs: number;
maxRequests: number;
}
const store = new Map<string, RateLimitEntry>();
export function rateLimiter(config: RateLimitConfig) {
return (req: Request, res: Response, next: NextFunction): void => {
const ip = req.ip
|| req.socket.remoteAddress || 'unknown';
const now = Date.now();
let entry = store.get(ip);
if (!entry || now > entry.resetTime) {
entry = { count: 1, resetTime: now + config.windowMs };
store.set(ip, entry);
next();
return;
}
entry.count++;
if (entry.count > config.maxRequests) {
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
res.set('Retry-After', String(retryAfter));
res.status(429).json({ error: 'Too many requests, please try again later.' });
return;
}
next();
};
}
// Cleanup old entries every minute
setInterval(() => {
const now = Date.now();
for (const [key, entry] of store.entries()) {
if (now > entry.resetTime) {
store.delete(key);
}
}
}, 60000);
Expert Prompts: Production-Grade Architectures
7. Generate a Custom React Context with Optimized Re-renders
Task: Create an auth context with user object and actions, but prevent unnecessary re-renders using useMemo and useCallback.
Prompt:
Create a React context for authentication. It should provide: user (object or null), login, logout, and isLoading. Use useReducer for state management. Optimize context value with useMemo and useCallback so that child components only re-render when relevant state changes. Export a custom hook useAuth that throws if used outside provider.
Example Result:
import React, { createContext, useContext, useReducer, useMemo, useCallback, ReactNode } from 'react';
interface User {
id: string;
name: string;
email: string;
}
interface AuthState {
user: User | null;
isLoading: boolean;
}
type AuthAction =
| { type: 'LOGIN_START' }
| { type: 'LOGIN_SUCCESS'; payload: User }
| { type: 'LOGIN_FAILURE' }
| { type: 'LOGOUT' };
interface AuthContextValue {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case 'LOGIN_START':
return { ...state, isLoading: true };
case 'LOGIN_SUCCESS':
return { user: action.payload, isLoading: false };
case 'LOGIN_FAILURE':
return { user: null, isLoading: false };
case 'LOGOUT':
return { user: null, isLoading: false };
default:
return state;
}
}
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, { user: null, isLoading: false });
const login = useCallback(async (email: string, password: string) => {
dispatch({ type: 'LOGIN_START' });
try {
// Simulate API call
const response = await new Promise<User>((resolve) => {
setTimeout(() => resolve({ id: '1', name: 'John Doe', email }), 1000);
});
dispatch({ type: 'LOGIN_SUCCESS', payload: response });
} catch {
dispatch({ type: 'LOGIN_FAILURE' });
throw new Error('Login failed');
}
}, []);
const logout = useCallback(() => {
dispatch({ type: 'LOGOUT' });
}, []);
const contextValue = useMemo(() => ({
user: state.user,
isLoading: state.isLoading,
login,
logout,
}), [state.user, state.isLoading, login, logout]);
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
};
export function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
8. Generate a Full TypeScript Express Middleware Pipeline
Task: Create a middleware pipeline for authentication, logging, and error handling.
Prompt:
Write a production-grade Express middleware pipeline in TypeScript that includes: 1) request logging (method, url, duration), 2) JWT authentication middleware, 3) global error handler. Use async/await, proper typing, and export as a single function that accepts a logger instance.
Example Result:
import { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express';
import jwt from 'jsonwebtoken';
interface Logger {
info: (message: string) => void;
error: (message: string) => void;
}
interface AuthenticatedRequest extends Request {
userId?: string;
}
export function createMiddlewarePipeline(logger: Logger) {
// Request logging middleware
const requestLogger: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info(`${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`);
});
next();
};
// JWT authentication middleware
const authenticate: RequestHandler = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid authorization header' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'default_secret') as { userId: string };
req.userId = decoded.userId;
next();
} catch (err) {
logger.error(`JWT verification failed: ${(err as Error).message}`);
return res.status(401).json({ error: 'Invalid or expired token' });
}
};
// Global error handler
const errorHandler: ErrorRequestHandler = (err: Error, req: Request, res: Response, _next: NextFunction) => {
logger.error(`Unhandled error: ${err.message}`);
res.status(500).json({ error: 'Internal server error' });
};
return {
requestLogger,
authenticate,
errorHandler,
};
}
9. Generate a TypeScript Generic Repository Pattern
Task: Create a base repository class for CRUD operations with PostgreSQL.
Prompt:
Create a TypeScript generic base repository class using the pg Pool. It should implement findAll, findById, create, update, delete methods. Include proper typing with generics. Use parameterized queries to prevent SQL injection. The class should be abstract and require a tableName in constructor.
Example Result:
import { Pool, QueryResult } from 'pg';
export abstract class BaseRepository<T extends { id: number }> {
protected pool: Pool;
protected tableName: string;
constructor(pool: Pool, tableName: string) {
this.pool = pool;
this.tableName = tableName;
}
async findAll(): Promise<T[]> {
const result: QueryResult<T> = await this.pool.query(`SELECT * FROM ${this.tableName}`);
return result.rows;
}
async findById(id: number): Promise<T | null> {
const result: QueryResult<T> = await this.pool.query(
`SELECT * FROM ${this.tableName} WHERE id = $1`,
[id]
);
return result.rows[0] || null;
}
async create(data: Omit<T, 'id'>): Promise<T> {
const keys = Object.keys(data as Record<string, unknown>);
const values = Object.values(data as Record<string, unknown>);
const placeholders = keys.map((_, index) => `$${index + 1}`).join(', ');
const columns = keys.join(', ');
const result: QueryResult<T> = await this.pool.query(
`INSERT INTO ${this.tableName} (${columns}) VALUES (${placeholders}) RETURNING *`,
values
);
return result.rows[0];
}
async update(id: number, data: Partial<T>): Promise<T | null> {
const keys = Object.keys(data as Record<string, unknown>);
const values = Object.values(data as Record<string, unknown>);
const setClause = keys.map((key, index) => `${key} = $${index + 2}`).join(', ');
const result: QueryResult<T> = await this.pool.query(
`UPDATE ${this.tableName} SET ${setClause} WHERE id = $1 RETURNING *`,
[id, ...values]
);
return result.rows[0] || null;
}
async delete(id: number): Promise<boolean> {
const result: QueryResult = await this.pool.query(
`DELETE FROM ${this.tableName} WHERE id = $1`,
[id]
);
return (result.rowCount ?? 0) > 0;
}
}
10. Generate a React Component with Suspense and Error Boundary
Task: Create a data-fetching component using React Suspense and a custom error boundary.
Prompt:
Write a React component that fetches user data using Suspense (with a custom resource fetcher). Wrap it in an ErrorBoundary class component. Use TypeScript. The resource fetcher should cache results and throw a promise while loading.
Example Result:
import React, { Suspense, Component, ReactNode } from 'react';
// Resource fetcher (simple cache)
function createResource<T>(fetchFn: () => Promise<T>) {
let status: 'pending'
| 'success' | 'error' = 'pending';
let result: T;
let error: Error;
const suspender = fetchFn().then(
(data) => {
status = 'success';
result = data;
},
(err) => {
status = 'error';
error = err;
}
);
return {
read(): T {
if (status === 'pending') throw suspender;
if (status === 'error') throw error;
return result;
},
};
}
interface User {
id: number;
name: string;
}
const userResource = createResource<User>(
() => new Promise((resolve) => setTimeout(() => resolve({ id: 1, name: 'Alice' }), 2000))
);
const UserProfile: React.FC = () => {
const user = userResource.read();
return <div>User: {user.name}</div>;
};
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return this.props.fallback || <div>Something went wrong: {this.state.error?.message}</div>;
}
return this.props.children;
}
}
const App: React.FC = () => {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
};
export default App;
Best Practices for Crafting Your Own Prompts
- Be specific about the environment: Mention the framework version, Node.js version, and any libraries you expect to use. For instance, "React 18 with TypeScript 5.0 and Vite" yields more accurate results.
- Define input and output types explicitly: For TypeScript, always ask for generics and proper typing. This reduces type errors significantly.
- Include edge cases: Ask for error handling, null checks, and boundary conditions. "Handle empty array, invalid input, and network failure" forces the AI to write robust code.
- Request code style preferences: "Use functional components with hooks, not classes" or "Use async/await, not .then()".
- Ask for tests: Add "Include unit tests using Jest and React Testing Library" to get testable code.
Conclusion
Generating JavaScript and TypeScript code with AI is no longer a novelty — it's a daily productivity booster for developers at all levels. The key lies in crafting precise, context-rich prompts that communicate your exact needs. From simple utility functions to complex React contexts and repository patterns, the 15 prompts in this guide give you a solid foundation.
As AI models continue to improve, the quality of generated code will only get better. Start by adapting these prompts to your own projects, and soon you'll develop an intuition for what works. Remember: the best prompt is the one that produces code you can trust — and with TypeScript's type system, you can verify that trust immediately.
Happy coding, and may your prompts always compile!
Comments