Blog Laravel

API REST con Laravel y Sanctum: clientes, NIT y operaciones en lote

Construimos una API REST completa para gestión de clientes colombianos usando Laravel y Sanctum. CRUD completo con validación de NIT único, inserción masiva con bulkStore y autenticación por token. Todo probado con Insomnia.

01

¿Qué vamos a construir?

Una API REST para gestionar clientes con los campos que se usan en proyectos colombianos reales: nombre, NIT (con dígito de verificación), email, teléfono, dirección y estado activo.

Al final de este tutorial tendrás estos endpoints funcionando y probados con Insomnia:

GET    /api/clientes          → lista todos los clientes
POST   /api/clientes          → crea un cliente
GET    /api/clientes/{id}     → obtiene un cliente por ID
PUT    /api/clientes/{id}     → actualiza un cliente
DELETE /api/clientes/{id}     → elimina un cliente
POST   /api/clientes/bulk     → inserción masiva
GET    /api/user              → usuario autenticado (protegido con Sanctum)
02

Crear el proyecto e instalar Sanctum

Creamos un proyecto Laravel limpio y habilitamos el soporte para APIs con Sanctum:

composer create-project laravel/laravel api-practica
cd api-practica
php artisan install:api

El comando install:api hace tres cosas automáticamente: publica la migración de personal_access_tokens, agrega la configuración de Sanctum y crea el archivo routes/api.php si no existe.

Con Laravel Herd el dominio local queda en http://api-practica.test. Si usás php artisan serve queda en http://localhost:8000. Todos los endpoints de la API van bajo /api/.
03

La migración

Creamos la migración para la tabla de clientes:

php artisan make:migration create_clientes_table

El campo clave es nit con restricción unique() — en Colombia no pueden existir dos empresas con el mismo NIT ante la DIAN:

Schema::create('clientes', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('nit')->unique();
    $table->string('email')->nullable();
    $table->string('phone')->nullable();
    $table->string('address')->nullable();
    $table->boolean('active')->default(true);
    $table->timestamps();
});
php artisan migrate
El NIT colombiano tiene el formato XXXXXXXXX-X (nueve dígitos más dígito de verificación). Lo guardamos como string para preservar ese formato exacto. Para validación avanzada del algoritmo DIAN podés usar el paquete jamesmosq/laravel-fiscal-colombia.
04

El Modelo

php artisan make:model Cliente
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Cliente extends Model
{
    protected $fillable = [
        'name',
        'nit',
        'email',
        'phone',
        'address',
        'active',
    ];
}
05

El Controlador

Generamos un controlador de API — usa --api en vez de --resource para omitir los métodos create y edit que no tienen sentido en una API REST:

php artisan make:controller ClienteController --api

Completamos cada método en app/Http/Controllers/ClienteController.php:

<?php

namespace App\Http\Controllers;

use App\Models\Cliente;
use Illuminate\Http\Request;

class ClienteController extends Controller
{
    public function index()
    {
        return response()->json(Cliente::all());
    }

    public function store(Request $request)
    {
        $request->validate([
            'name'    => 'required|string',
            'nit'     => 'required|string|unique:clientes,nit',
            'email'   => 'nullable|email',
            'phone'   => 'nullable|string',
            'address' => 'nullable|string',
        ]);

        $cliente = Cliente::create($request->all());

        return response()->json($cliente, 201);
    }

    public function show(Cliente $cliente)
    {
        return response()->json($cliente);
    }

    public function update(Request $request, Cliente $cliente)
    {
        $request->validate([
            'name'    => 'sometimes|string',
            'nit'     => 'sometimes|string|unique:clientes,nit,' . $cliente->id,
            'email'   => 'nullable|email',
            'phone'   => 'nullable|string',
            'address' => 'nullable|string',
        ]);

        $cliente->update($request->all());

        return response()->json($cliente);
    }

    public function destroy(Cliente $cliente)
    {
        $cliente->delete();

        return response()->json(['message' => 'Cliente eliminado correctamente']);
    }

    public function bulkStore(Request $request)
    {
        $request->validate([
            '*.name' => 'required|string',
            '*.nit'  => 'required|string|distinct',
        ]);

        $clientes = collect($request->all())->map(function ($item) {
            return array_merge($item, [
                'active'     => true,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
        })->toArray();

        Cliente::insert($clientes);

        return response()->json(['message' => 'Clientes creados correctamente'], 201);
    }
}
En update() el NIT usa la regla unique:clientes,nit,{id} para ignorar el propio registro al validar unicidad — sin esto, actualizar cualquier otro campo fallaría por el NIT existente del mismo cliente.
06

Las rutas

En routes/api.php registramos el resource y la ruta personalizada para inserción masiva:

<?php

use App\Http\Controllers\ClienteController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

// Ruta protegida con Sanctum
Route::get('/user', function (Request $request) {
    return $request->user();
})->middleware('auth:sanctum');

// Rutas de clientes
Route::post('clientes/bulk', [ClienteController::class, 'bulkStore']);
Route::apiResource('clientes', ClienteController::class);
La ruta clientes/bulk debe ir antes de apiResource. Si va después, Laravel interpreta "bulk" como el parámetro {cliente} de la ruta show y nunca llega al método correcto.

Verificá las rutas generadas:

php artisan route:list --path=api
GET    api/clientes              clientes.index
POST   api/clientes              clientes.store
GET    api/clientes/{cliente}    clientes.show
PUT    api/clientes/{cliente}    clientes.update
DELETE api/clientes/{cliente}    clientes.destroy
POST   api/clientes/bulk         (bulk)
07

Autenticación con Sanctum

Sanctum ya está instalado. Para proteger las rutas de clientes con autenticación por token, envolvés las rutas en el middleware auth:sanctum:

Route::middleware('auth:sanctum')->group(function () {
    Route::post('clientes/bulk', [ClienteController::class, 'bulkStore']);
    Route::apiResource('clientes', ClienteController::class);
});

Para generar un token desde Tinker y probarlo en Insomnia:

php artisan tinker

$user = App\Models\User::first();
$token = $user->createToken('insomnia-test')->plainTextToken;
echo $token;

En Insomnia agregás el header:

Authorization: Bearer 1|tu_token_aqui
08

Probando con Insomnia

Con el servidor corriendo, estos son los resultados reales de cada endpoint:

GET /api/clientes — lista todos los clientes:

[
    {
        "id": 1,
        "name": "Empresa Laravel SAS",
        "nit": "900111333-5",
        "email": "contacto@laraveltest.com",
        "phone": "3001234567",
        "address": "Calle 10 # 20-30, Medellín",
        "active": 1,
        "created_at": "2026-05-09T23:32:54.000000Z",
        "updated_at": "2026-05-09T23:32:54.000000Z"
    },
    {
        "id": 2,
        "name": "Ferretería El Tornillo SAS",
        "nit": "900111222-1",
        "email": "contacto@tornillo.com",
        "phone": "3101234567",
        "address": "Calle 50 # 10-20, Medellín",
        "active": 1,
        "created_at": "2026-05-09T23:41:31.000000Z",
        "updated_at": "2026-05-09T23:41:31.000000Z"
    }
]

POST /api/clientes — crear un cliente (body JSON):

{
    "name": "Constructora Palma Real SAS",
    "nit": "901234567-3",
    "email": "cxp@palmareal.com.co",
    "phone": "6044321098",
    "address": "Cl 10 # 43E-31, Medellín"
}

Respuesta 201 Created:

{
    "id": 7,
    "name": "Constructora Palma Real SAS",
    "nit": "901234567-3",
    "email": "cxp@palmareal.com.co",
    "phone": "6044321098",
    "address": "Cl 10 # 43E-31, Medellín",
    "active": 1,
    "created_at": "2026-05-09T23:45:37.000000Z",
    "updated_at": "2026-05-09T23:45:37.000000Z"
}

POST /api/clientes/bulk — inserción masiva (array de objetos):

[
    {
        "name": "Distribuidora Andina Ltda",
        "nit": "800543210-2",
        "email": "ventas@andina.com",
        "phone": "3157654321",
        "address": "Cra 30 # 8-61, Bogotá"
    },
    {
        "name": "Inversiones Pacífico SA",
        "nit": "890765432-4",
        "email": "tesoreria@invpacifico.com",
        "phone": "6022109876",
        "address": "Av. 6N # 23A-26, Cali"
    },
    {
        "name": "Logística Express Colombia SAS",
        "nit": "900543210-5",
        "email": "admin@logexpress.co",
        "phone": "3157654322",
        "address": "Cra 30 # 8-61, Bogotá"
    }
]

Respuesta 201 Created:

{ "message": "Clientes creados correctamente" }

DELETE /api/clientes/{id}:

{ "message": "Cliente eliminado correctamente" }
jamesmosq / api-practica
Código completo de este tutorial — API REST Laravel con Sanctum, clientes colombianos con NIT y endpoint de inserción masiva.
Ver en GitHub
Volver al blog