Skip to main content

Architecture

The Tanqueo Backend API follows a layered architecture pattern with clear separation of concerns between routing, business logic, and data access.

System Overview

Technology Stack

Core Technologies

LayerTechnologyVersionPurpose
RuntimeNode.js18+Server runtime environment
LanguageTypeScript5.9.3Type-safe development
FrameworkExpress.js5.2.1Web application framework
DatabasePostgreSQLvia SupabaseRelational data storage
AuthSupabase Auth2.89.0User authentication & JWT
File UploadMulter2.0.2Multipart form data handling
CORScors2.8.5Cross-origin resource sharing

Package Configuration

package.json
{
  "name": "tanqueo-backend",
  "version": "1.0.0",
  "main": "dist/server.js",
  "type": "commonjs",
  "scripts": {
    "start": "node dist/server.js",
    "dev": "nodemon --exec ts-node src/server.ts",
    "build": "tsc"
  }
}

Application Structure

Directory Layout

tanqueo-backend/
├── src/
│   ├── config/
│   │   └── supabase.ts          # Supabase client configuration
│   ├── controllers/
│   │   ├── auth.controller.ts
│   │   ├── tanqueos.controller.ts
│   │   ├── engrases.controller.ts
│   │   ├── documentos.controller.ts
│   │   ├── flota.controller.ts
│   │   ├── mantenimiento.controller.ts
│   │   ├── presupuestos.controller.ts
│   │   ├── upload.controller.ts
│   │   └── ...
│   ├── middleware/
│   │   └── auth.middleware.ts   # JWT authentication
│   ├── routes/
│   │   ├── auth.routes.ts
│   │   ├── tanqueos.routes.ts
│   │   ├── engrases.routes.ts
│   │   └── ...
│   ├── types/
│   │   └── index.ts             # TypeScript interfaces
│   └── server.ts                # Application entry point
├── dist/                        # Compiled JavaScript
├── package.json
├── tsconfig.json
└── .env                         # Environment variables

Server Initialization

The Express application is initialized in src/server.ts:
src/server.ts
import dotenv from 'dotenv';

// IMPORTANT: Load environment variables FIRST
dotenv.config();

import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import authRoutes from './routes/auth.routes';
import tanqueosRoutes from './routes/tanqueos.routes';
// ... other route imports

const app = express();
const PORT = process.env.PORT || 5000;

// Middleware setup
app.use(cors({
  origin: process.env.FRONTEND_URL || '*',
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

app.use(express.json());

// Request logging middleware
app.use((req: Request, res: Response, next: NextFunction) => {
  console.log(`${req.method} ${req.path}`);
  next();
});

// Route registration
app.use('/api/auth', authRoutes);
app.use('/api/tanqueos', tanqueosRoutes);
app.use('/api/engrases', engrasesRoutes);
// ... other routes

// Health check endpoint
app.get('/api/health', (req: Request, res: Response) => {
  res.json({
    status: 'OK',
    message: 'Servidor funcionando correctamente',
    timestamp: new Date().toISOString()
  });
});

// 404 handler
app.use((req: Request, res: Response) => {
  res.status(404).json({ error: 'Ruta no encontrada' });
});

app.listen(PORT, () => {
  console.log(`🚀 Servidor corriendo en puerto ${PORT}`);
  console.log(`📊 Health check: http://localhost:${PORT}/api/health`);
});

Middleware Pattern

Middleware Execution Order

1. CORS Middleware

Configured to accept requests from the frontend URL:
app.use(cors({
  origin: process.env.FRONTEND_URL || '*',
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

2. JSON Body Parser

Parses incoming JSON payloads:
app.use(express.json());

3. Request Logger

Logs all incoming requests:
app.use((req: Request, res: Response, next: NextFunction) => {
  console.log(`${req.method} ${req.path}`);
  next();
});

4. Authentication Middleware

Applied to protected routes:
src/middleware/auth.middleware.ts
export async function authMiddleware(
  req: AuthRequest,
  res: Response,
  next: NextFunction
): Promise<void> {
  const token = req.headers.authorization?.replace('Bearer ', '');
  
  if (!token) {
    res.status(401).json({ error: 'Token no proporcionado' });
    return;
  }
  
  const { data: { user }, error } = await supabase.auth.getUser(token);
  
  if (error || !user) {
    res.status(401).json({ error: 'Token inválido' });
    return;
  }
  
  // Attach user and authenticated client to request
  req.user = userData;
  req.accessToken = token;
  req.supabase = createAuthClient(token);
  
  next();
}

Route Organization

Route Definition Pattern

All routes follow a consistent pattern:
src/routes/tanqueos.routes.ts
import { Router } from 'express';
import { tanqueosController } from '../controllers/tanqueos.controller';
import { authMiddleware } from '../middleware/auth.middleware';

const router = Router();

// All routes require authentication
router.use(authMiddleware);

// CRUD operations
router.get('/', tanqueosController.getAll);
router.get('/dashboard', tanqueosController.getDashboard);
router.get('/filter-options', tanqueosController.getFilterOptions);
router.get('/:id', tanqueosController.getById);
router.post('/', tanqueosController.create);
router.put('/:id', tanqueosController.update);
router.delete('/:id', tanqueosController.delete);

export default router;

API Route Structure

Base PathModuleAuthentication Required
/api/authAuthenticationNo (login/refresh)
/api/tanqueosFuel trackingYes
/api/engrasesMaintenanceYes
/api/documentosDocumentsYes
/api/flotaFleet managementYes
/api/mantenimientoMaintenance plansYes
/api/presupuestosBudgetsYes
/api/catalogosMaster dataYes
/api/uploadFile uploadsYes
/api/saldos-bombasPump balancesYes
/api/healthHealth checkNo

Controller Pattern

Controller Structure

Controllers implement business logic and database interactions:
export const tanqueosController = {
  async getAll(req: AuthRequest, res: Response): Promise<void> {
    try {
      // Extract query parameters
      const page = parseInt(req.query.page as string) || 1;
      const limit = parseInt(req.query.limit as string) || 20;
      
      // Build database query
      let query = supabase.from('tanqueo_relaciones').select('*', { count: 'exact' });
      
      // Apply filters, sorting, pagination
      const { data, error, count } = await query.range(offset, offset + limit - 1);
      
      if (error) {
        res.status(400).json({ error: error.message });
        return;
      }
      
      res.json({
        data,
        pagination: {
          page,
          limit,
          total: count || 0,
          totalPages: Math.ceil((count || 0) / limit)
        }
      });
    } catch (error) {
      console.error('Error:', error);
      res.status(500).json({ error: 'Error en el servidor' });
    }
  },
  
  async create(req: AuthRequest, res: Response): Promise<void> { /* ... */ },
  async update(req: AuthRequest, res: Response): Promise<void> { /* ... */ },
  async delete(req: AuthRequest, res: Response): Promise<void> { /* ... */ }
};

Database Layer

Supabase Client Configuration

src/config/supabase.ts
import { createClient, SupabaseClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_ANON_KEY;

if (!supabaseUrl || !supabaseKey) {
  throw new Error('SUPABASE_URL y SUPABASE_ANON_KEY deben estar definidos');
}

// Anonymous client for auth operations
export const supabase = createClient(supabaseUrl, supabaseKey);

// Create authenticated client with user token (for RLS)
export function createAuthClient(accessToken: string): SupabaseClient {
  return createClient(supabaseUrl, supabaseKey, {
    global: {
      headers: {
        Authorization: `Bearer ${accessToken}`
      }
    }
  });
}

Row Level Security (RLS)

The authenticated client (req.supabase) automatically applies PostgreSQL RLS policies:
  • Users can only access data they’re authorized to see
  • Database-level security enforcement
  • No additional authorization logic needed in application code

Database Views

Complex queries use PostgreSQL views for performance:
  • tanqueo_relaciones - Joins tanqueos with related tables
  • engrase_relaciones - Joins engrases with conductors, placas, areas
  • documento_relaciones - Joins documents with vehicle data

Request/Response Flow

Error Handling

Consistent error handling across all controllers:
try {
  // Business logic
  const { data, error } = await supabase.from('table').select();
  
  if (error) {
    res.status(400).json({ error: error.message });
    return;
  }
  
  res.json({ data });
} catch (error) {
  console.error('Error:', error);
  res.status(500).json({ error: 'Error en el servidor' });
}
See Error Handling for detailed error patterns.

Environment Configuration

Required environment variables:
.env
# Server
PORT=5000
FRONTEND_URL=http://localhost:3000

# Supabase
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key

Performance Considerations

Caching

Implemented in-memory caching for frequently accessed data:
// 5-minute cache for filter options
let filterOptionsCache: { data: any; timestamp: number } | null = null;
const CACHE_TTL = 5 * 60 * 1000;

if (filterOptionsCache && Date.now() - filterOptionsCache.timestamp < CACHE_TTL) {
  return filterOptionsCache.data;
}

Pagination

All list endpoints support pagination:
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 20;
const offset = (page - 1) * limit;

query.range(offset, offset + limit - 1);

Database Indexing

Optimized queries rely on PostgreSQL indexes for:
  • Foreign key relationships
  • Date range queries
  • Text search fields

Deployment

The application is designed for deployment on platforms like:
  • Railway (automatic PORT environment variable)
  • Heroku
  • Render
  • AWS / GCP / Azure with Node.js runtime
# Build for production
npm run build

# Start production server
npm start

Build docs developers (and LLMs) love