DEV Community

Cover image for Build Express APIs Faster than AI
Ahmed Rakan
Ahmed Rakan

Posted on

Build Express APIs Faster than AI

Adhering to software principles ensures project longevity and enables faster delivery of high-quality code—often faster than any code generation tool.

Can we structure our codebase to ship an API or endpoint in under a minute? The short answer: yes, it’s possible.

By following clean code principles, we can design an efficient ExpressJS, MongoDB, and TypeScript API. Combining functional and object-oriented programming, we’ll create a codebase that’s simple to develop and enjoyable to collaborate on, fostering both efficiency and a positive development experience.

Technology Stack:

  1. ExpressJS: API endpoints
  2. TypeScript: Type safety and robustness
  3. Mongoose: MongoDB interaction

Code Structure:

  1. Model: Defines entity schema
  2. Entity: Core logic and contract
  3. BaseRepository: Common database operations
  4. BaseService: Shared business logic
  5. BaseController: Common CRUD operations
  6. AsyncControllerCreator: Generates async controllers with error handling
  7. RouterCreator: Creates routers for entities
  8. ValidatorCreator: Validates entity-specific endpoints

This architecture fosters reusable, extensible components, ensuring rapid feature delivery while maintaining flexibility for future changes.

Our folder structure will reflect this approach.

Image description

So one thing to really understand here, generic.ts files, for example in repositories the Repository class is generic class that can extend any repository with needed functionality of find, create, update delete method of our database, it can accept any model, this is how we create our reusable logic, in JS we can use mixin for multi-inheritance, to extend derived classes further.

The second thing to understand is index.ts file, which where we create an instance of our app entity repositories, services, controllers.

The full source code can be found here : https://github.com/ARAldhafeeri/ExpressJS-1minute-crud

Let's explain ,

first global types

import { Request, Response, Express, NextFunction } from 'express';

declare global {
    type APIResponse<Data> = { data: Data; message: string; status: boolean };
    type Controller = (req: Request, res: Response) => void;
    type Middleware = (req: Request, res: Response, next: NextFunction) => void;
}

Enter fullscreen mode Exit fullscreen mode

here we created reusable global types to use across the source code.

utils/ControllerCreator.ts

this helps us to remove redundant error handling and focus on implementation when we write our code and return the error via throwing it.

import { Request, Response } from 'express';

export const ControllerCreator =
    (fn: Controller ) => (req: Request, res: Response) => {
        Promise.resolve(fn(req, res)).catch((err: Error) => {
            res.status(400).send({ status: false, message: `Error: ${err.message}` });
        });
    };
Enter fullscreen mode Exit fullscreen mode

utils/RouterCreator
this helps us to create reduant curd apis, we can create multiple of those , this one takes a controller, we can create one that takes a router and extend it for reuseable endpoints across diffrent entities. Note how we return the router , so we can extended with endpoints specific for that entity.

import { Router } from 'express';
import {IController} from "../entities/generic";

export const RouterCreator = (controller: IController): Router => {
    const router = Router();

    controller.fetch &&  router.get('/', (req, res) => controller.fetch!(req, res));
    controller.create && router.post('/', (req, res) => controller.create!(req, res));
    controller.update && router.put('/', (req, res) => controller.update!(req, res));
    controller.delete &&  router.delete('/', (req, res) => controller.delete!(req, res));

    return router;
};
Enter fullscreen mode Exit fullscreen mode

Note here only if the controller define the endpoint within it's implementation it gets the endpoint.

repositories/generics.ts
here we create the base repo, note in child class we can access the model and further extend it with functionality related to the child class

import { IRepository } from '../entities/generic';
import {Model, Document, UpdateQuery} from 'mongoose';
import { FilterQuery, ProjectionType, Types } from 'mongoose';

export class Repository<T> implements IRepository<any> {
    constructor(private model: Model<T>) {}

    async find(
        filter: FilterQuery<T>,
        projection: ProjectionType<T>,
    ): Promise<T[]> {
        return this.model.find(filter, projection)
            .sort({ updatedAt: -1 })
    }

    async create(record: T): Promise<any> {
        const newRecord = new this.model(record);
        return newRecord.save();
    }

    async update(
        filter: FilterQuery<T>,
        record: UpdateQuery<T>,
    ): Promise<any> {
        return this.model.findByIdAndUpdate(filter, record, { new: true }).exec();
    }

    async delete(filter: FilterQuery<T>): Promise<any> {
        return this.model.findOneAndDelete(filter).exec();
    }
}
Enter fullscreen mode Exit fullscreen mode

entities/generics.ts

import {FilterQuery, ProjectionType, Types, UpdateQuery} from 'mongoose';

export interface IBaseEntity {
    _id?: Types.ObjectId;
    createdAt?: Date;
    updatedAt?: Date;
}


export interface IRepository<T extends IBaseEntity> {
    find(
        filter: FilterQuery<T>,
        projection: ProjectionType<T>,
    ): Promise<T[]>;
    create(record: T): Promise<T>;
    update(
        filter: FilterQuery<T>,
        record: UpdateQuery<T>,
    ): Promise<T>;
    delete(
        filter: FilterQuery<T>,
    ): Promise<T>;
}

export interface IService<T extends IBaseEntity> {
    find(id: string ): Promise<T[]>;
    create(record: T, id: string): Promise<T>;
    update(
        record: T,
        recordID: string
    ): Promise<T>;

    delete(id: string, organization: string): Promise<T>;
}

export interface IController {
    fetch ?: Controller;
    create?: Controller;
    update?: Controller;
    delete?: Controller;
    search?: Controller;
}
Enter fullscreen mode Exit fullscreen mode

services/generic.ts
here we create base service, same with repo, we can access the repo via super

import { IService, IBaseEntity, IRepository } from '../entities/generic';
import {Types} from "mongoose";
import ObjectId = Types.ObjectId;

export class Service<T extends IBaseEntity> implements IService<T> {
    constructor(protected repository: IRepository<T>) {
    }

    async find(organization: string ): Promise<any> {
        const filter = { organization: organization };
        return  this.repository.find(filter, {});

    }

    async create(record: T, organization: string): Promise<T> {
        return this.repository.create(record);
    }

    async update(
        record: T,
        recordID: string
    ): Promise<T> {
        const filter = {
            _id: new ObjectId(recordID),
        }
        return this.repository.update(filter, record);
    }

    async delete(id: string): Promise<T> {
        return this.repository.delete({_id: id});
    }

}
Enter fullscreen mode Exit fullscreen mode

controllers/generic.ts

import { Request, Response } from 'express';
import {ControllerCreator} from "../utils/ControllerCreator";

class Controller<T> {
    constructor(protected service: T) {
        this.service = service;
    }

    fetch = ControllerCreator(async (req: Request, res: Response) => {

        const data = await (this.service as any).find();
        res.status(200).json({ data, status: true, message: 'Data fetched' });
    });

    create = ControllerCreator(async (req: Request, res: Response) => {
        const data = await (this.service as any).create(req.body);
        res.status(201).json({ data, status: true, message: 'Created successfully' });
    });

    update = ControllerCreator(async (req: Request, res: Response) => {
        const data = await (this.service as any).update(req.body, req.query.id as string);
        res.status(200).json({ data, status: true, message: 'Updated successfully' });
    });

    delete = ControllerCreator(async (req: Request, res: Response) => {
        const data = await (this.service as any).delete(req.query.id as string);
        res.status(200).json({ data, status: true, message: 'Deleted successfully' });
    });
}

export default Controller;

Enter fullscreen mode Exit fullscreen mode

we have created base contract for our base repo, service, controller , for other classes to inherit from.

now here is the result , we can ship multiple curd APIs, for multiple entities with less than 10 lines of code, note index.ts files within each directory we just create instances so our app is memory optimized.

entities/user.ts
we created the contract for user, also we kept it open for extension.

import {IBaseEntity, IController, IRepository, IService} from "./generic";

export interface IUser extends IBaseEntity {
    name: string;
    address: string;
}

export interface IUserRepository extends IRepository<IUser> {

}

export interface IUserService extends IService<IUser>{

}

export interface IUserController extends IController {}

Enter fullscreen mode Exit fullscreen mode

models/user.ts
siimple mongose model

import { Schema, model } from 'mongoose';
import { IUser } from '../entities/user';

export const userSchema = new Schema<IUser>(
    {
        name: { type: String },
        address: { type: String },
    },
    { timestamps: true }
);

userSchema.index({ name: 'text', address: 'text' });

export default model<IUser>('User', userSchema);

Enter fullscreen mode Exit fullscreen mode

repoisitories/user.ts
we use the core functionality of find, create, update, delete from base repository and we keep the user repository open for extension

import { Repository } from "./generic";
import {IUser, IUserRepository} from "../entities/user";


class UserRepository extends Repository<IUser> implements IUserRepository {

}

export default UserRepository;

Enter fullscreen mode Exit fullscreen mode

services/user.ts

import {Service} from "./generic";
import {IUser, IUserService} from "../entities/user";


class UserService extends Service<IUser> implements IUserService {

}

export default UserService;
Enter fullscreen mode Exit fullscreen mode

controllers/user.ts

import Controller from "./generic";
import {IUserController, IUserService} from "../entities/user";


class UserController extends Controller<IUserService> implements IUserController {

}

export default UserController;
Enter fullscreen mode Exit fullscreen mode

routes/user.ts

import {RouterCreator} from "../utils/RouterCreator";
import {userController} from "../controllers";

const UserRouter = RouterCreator(userController);

export default UserRouter;
Enter fullscreen mode Exit fullscreen mode

app.ts

import express, {Application} from "express";
import UserRouter from "./routes/user";


const App : Application = express();

App.use("/user", UserRouter);

export default App;
Enter fullscreen mode Exit fullscreen mode

I hope you get the idea how powerful this is, we can create multiple reusable code blocks across the source code, across any layer, this will lead to robust source code and very maintainable, readable, bug-free developer experience.

Best regards,

Ahmed,

Top comments (0)