· Andrés Ledo · PHP · Lectura en 15 min
Tutorial Laravel 8 API REST con autentificación JWT (CRUD)
En este tutorial veremos como podemos crear un API rest con Autentificación de usuario en Laravel.
Índice
- Requisitos
- Instalación de Laravel
- Instalar Tymon JWTAuth
- Configuración Tymon JWTAuth
- Configuración base de datos y migraciones
- Creación del AuthController
- Creación del ProductsController
- Creación del Middleware
- Configurar el fichero de rutas
- Probando la API REST con Postman
- Código fuente Laravel 8 API REST con autentificación
Requisitos
Para seguir el tutorial utilizaremos una serie de herramientas que seguramente debas de tener instalado en tu equipo. Las herramientas son las siguientes:
- Composer: para instalar los paquetes y dependencias.
- Un editor de código: recomiendo Visual Studio Code.
- Un entorno de desarrollo (opcional): personalmente utilizo Laragon, pero puedes utilizar cualquier otro o incluso el comando «php artisan serve».
- Una base de datos MySQL/MariaDB: Si utilizas Laragon, Xamp, Wamp, etc ya te incluye la instalación de gestor de base de datos.
- Un visualizador de base de datos: En mi caso utilizo Heidi, pero puedes utilizar el PHPMyAdmin si te sientes más cómodo con él.
- Postman: Para probar nuestra API, si utilizas otro eres libre de usarlo.
- Conocimientos básicos del funcionamiento de Laravel.
Instalación de Laravel
Para realizar las pruebas te recomiendo realizar una instalación limpia de Laravel 8, si ejecutas este procedimiento en una instalación que ya hayas instalado algún paquete es posible que tengas algun conflicto.
El primer paso es asegurarnos es descargar la última versión del instalador de Laravel, para ello ejecutamos el siguiente comando en un terminal:
composer global require laravel/installer
Ahora crearemos el proyecto «apirest» sobre el que realizaremos las pruebas de la API REST. En la terminal navegamos al directorio donde queramos realizar la instalación de Laravel y ejecutamos el siguiente comando:
laravel new apirest
Para acabar nos aseguramos que tengamos la versión de Laravel 8, navegamos al directorio «apirest» y ejecutamos el siguiente comando:
php artisan -V
Este tutorial se realiza sobre la versión 8 de Laravel que es la última el día que realizo el tutorial. Eso no significa que este tutorial de creación de una API REST no sea válido para una versión superior de Laravel, es posible que cambien algunos pasos.
Instalar Tymon JWTAuth
Para realizar la autentificación mediante JWT (JSON Web Tokens) utilizaremos el paquete Tymon JWTAuth, he probado varios y este es el que mejor resultado me ha dado. Básicamente seguiremos la documentación oficial, así que tenla presente.
Para realizar la instalación del paquete escribimos el siguiente comando (recuerda que debes ejecutarlo desde el directorio de Laravel):
composer require tymon/jwt-auth
Después de haber instalado el paquete publicamos la configuración por si más adelante debemos realizar alguna modificación en la configuración. Para realizar esto ejecutamos el siguiente comando:
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
Por último, generaremos la Key con la que se cifraran los tokens, esto lo hacemos mediante el siguiente comando:
php artisan jwt:secret
Con esto ya hemos realizado todos los pasos de instalación del paquete. Recuerda que una vez que lleves este proyecto a producción tendrás que volver a generar la Key en el propio servidor de producción.
Configuración Tymon JWTAuth
No solo basta con la instalación del paquete, también debemos realizar una serie de modificaciones en nuestro Laravel 8 para que la autentificación de la API REST funcione correctamente.
Modificar User model
Debemos modificar el modelo de usuarios situado en app/Models/User.php
y en la instancia de la clase la parte de class User extends Authenticatable
debemos añadir al final implements JWTSubject
quedando así class User extends Authenticatable implements JWTSubject
.
Recuerda instanciar la clase que acabamos de implementar mediante use Tymon\JWTAuth\Contracts\JWTSubject;
.
Dentro de la clase debajo de la parte que hemos modificado debemos cambiar use HasApiTokens, HasFactory, Notifiable;
por use HasFactory, Notifiable;
. Para acabar eliminamos el use Laravel\Sanctum\HasApiTokens;
ya no lo necesitamos.
Para acabar en esta clase debemos de crear estas dos funciones públicas:
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
Básicamente tu User.php
debe de quedar tal que así:
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public function getJWTIdentifier()
{
return $this->getKey();
}
public function getJWTCustomClaims()
{
return [];
}
}
Modificar auth config
En el fichero config/auth.php
buscamos la parte en la que dice lo siguiente:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
Y la modificamos quedando de la siguiente forma:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
Creación de alias
Para acabar crearemos unos alias que nos facilitaran instancia las clases del paquete, para ello abrimos el archivo config/app.php
y en la parte de aliases agregamos lo siguiente al final:
//Customs
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
Quedando así la parte de los alias:
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'Date' => Illuminate\Support\Facades\Date::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
// 'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
//Customs
'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
],
Configuración base de datos y migraciones
No quiero realizar una aplicación demasiado complicada de entender, ya que la función de este artículo es que sea algo simple que puedas poner en práctica rápidamente.
Básicamente, realizaremos una aplicación que utilizará las migraciones de usuario incluidas con Laravel y crearemos una tabla de artículos.
Configurando la base de datos en Laravel
Deberías tener ya creada una base de datos MySQL o MariaDb en tu entorno de desarrollo ahora simplemente vamos a configurarla en el fichero .env que encontrarás en el directorio raíz.
Simplemente debes modificar estos parámetros con los datos de acceso a tu base de datos:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=apirest
DB_USERNAME=root
DB_PASSWORD=
Creación de la tabla Products
Empezamos creando la migración con el siguiente comando:
php artisan make:migration create_products_table
A continuación vamos a editar la migración que acabamos de crear, esta se encuentra en database/migrations/nombremigracion.php
. Modificamos todo el contenido de la migración por el siguiente:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProductsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name', 50);
$table->string('description', 150);
$table->decimal('stock');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('products');
}
}
Después ejecutamos todas las migraciones mediante el siguiente comando:
php artisan migrate
Si se ha ejecutado correctamente todo deberías tener una respuesta como la de la imagen, si revisas la base de datos con Heidi o PhpMyAdmin deberías de ver una tabla de users y la de products aparte de las que crea Laravel 8 para procesos internos.
Para acabar creamos el modelo que utilizaremos para trabajar con Eloquent con la tabla que acabamos de crear. Para ello ejecutamos el siguiente comando:
php artisan make:model Product
Este comando nos creará un modelo en App/Models/Product.php
lo modificamos por lo siguiente:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
'stock',
];
}
Creación del AuthController
A continuación crearemos el controlador que realizará toda la gestión con Tymon JWT Auth para registrar usuarios, para iniciar sesión y para cerrar la sesión. Empezamos creando el controlador mediante el siguiente comando:
php artisan make:controller V1/AuthController
Nos creará un controlador en app/Http/Controllers/V1/AuthController.php
.
Seguramente te preguntas por qué he creado el controlador en el directorio V1 en vez de en la raíz de Controllers, esto es importante hacerlo porque cuando haces una API nunca sabes las dimensiones que puede tener o si en un futuro quieres hacer grandes cambios.
Si en un futuro decidieras cambiar una gran parte de tu API puedes crear los cambios en un directorio V2 y dejar la versión vieja como referencia.
Volviendo al tema del artículo procedamos a modificar el controlador que acabamos de crear con lo siguiente:
<?php
namespace App\Http\Controllers\V1;
use App\Http\Controllers\Controller;
use JWTAuth;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Tymon\JWTAuth\Exceptions\JWTException;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Validator;
class AuthController extends Controller
{
//Función que utilizaremos para registrar al usuario
public function register(Request $request)
{
//Indicamos que solo queremos recibir name, email y password de la request
$data = $request->only('name', 'email', 'password');
//Realizamos las validaciones
$validator = Validator::make($data, [
'name' => 'required|string',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:6|max:50',
]);
//Devolvemos un error si fallan las validaciones
if ($validator->fails()) {
return response()->json(['error' => $validator->messages()], 400);
}
//Creamos el nuevo usuario
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password)
]);
//Nos guardamos el usuario y la contraseña para realizar la petición de token a JWTAuth
$credentials = $request->only('email', 'password');
//Devolvemos la respuesta con el token del usuario
return response()->json([
'message' => 'User created',
'token' => JWTAuth::attempt($credentials),
'user' => $user
], Response::HTTP_OK);
}
//Funcion que utilizaremos para hacer login
public function authenticate(Request $request)
{
//Indicamos que solo queremos recibir email y password de la request
$credentials = $request->only('email', 'password');
//Validaciones
$validator = Validator::make($credentials, [
'email' => 'required|email',
'password' => 'required|string|min:6|max:50'
]);
//Devolvemos un error de validación en caso de fallo en las verificaciones
if ($validator->fails()) {
return response()->json(['error' => $validator->messages()], 400);
}
//Intentamos hacer login
try {
if (!$token = JWTAuth::attempt($credentials)) {
//Credenciales incorrectas.
return response()->json([
'message' => 'Login failed',
], 401);
}
} catch (JWTException $e) {
//Error chungo
return response()->json([
'message' => 'Error',
], 500);
}
//Devolvemos el token
return response()->json([
'token' => $token,
'user' => Auth::user()
]);
}
//Función que utilizaremos para eliminar el token y desconectar al usuario
public function logout(Request $request)
{
//Validamos que se nos envie el token
$validator = Validator::make($request->only('token'), [
'token' => 'required'
]);
//Si falla la validación
if ($validator->fails()) {
return response()->json(['error' => $validator->messages()], 400);
}
try {
//Si el token es valido eliminamos el token desconectando al usuario.
JWTAuth::invalidate($request->token);
return response()->json([
'success' => true,
'message' => 'User disconnected'
]);
} catch (JWTException $exception) {
//Error chungo
return response()->json([
'success' => false,
'message' => 'Error'
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
//Función que utilizaremos para obtener los datos del usuario y validar si el token a expirado.
public function getUser(Request $request)
{
//Validamos que la request tenga el token
$this->validate($request, [
'token' => 'required'
]);
//Realizamos la autentificación
$user = JWTAuth::authenticate($request->token);
//Si no hay usuario es que el token no es valido o que ha expirado
if(!$user)
return response()->json([
'message' => 'Invalid token / token expired',
], 401);
//Devolvemos los datos del usuario si todo va bien.
return response()->json(['user' => $user]);
}
}
Creación del ProductsController
Ahora crearemos nuestro CRUD de productos, concretamente haremos que se puedan listar productos y ver uno en concreto sin autentificación y para editarlo, crear uno nuevo o eliminarlo, se necesitará autentificación.
Creamos el controlador con el siguiente comando:
php artisan make:controller V1/ProductsController
Nos creará un controlador en app/Controllers/V1/ProductsController.php
lo editamos con el siguiente código:
<?php
namespace App\Http\Controllers\V1;
use App\Http\Controllers\Controller;
use App\Models\Product;
use Illuminate\Http\Request;
use JWTAuth;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Validator;
class ProductsController extends Controller
{
protected $user;
public function __construct(Request $request)
{
$token = $request->header('Authorization');
if($token != '')
//En caso de que requiera autentifiación la ruta obtenemos el usuario y lo almacenamos en una variable, nosotros no lo utilizaremos.
$this->user = JWTAuth::parseToken()->authenticate();
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//Listamos todos los productos
return Product::get();
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//Validamos los datos
$data = $request->only('name', 'description', 'stock');
$validator = Validator::make($data, [
'name' => 'required|max:50|string',
'description' => 'required|max:50|string',
'stock' => 'required|numeric',
]);
//Si falla la validación
if ($validator->fails()) {
return response()->json(['error' => $validator->messages()], 400);
}
//Creamos el producto en la BD
$product = Product::create([
'name' => $request->name,
'description' => $request->description,
'stock' => $request->stock,
]);
//Respuesta en caso de que todo vaya bien.
return response()->json([
'message' => 'Product created',
'data' => $product
], Response::HTTP_OK);
}
/**
* Display the specified resource.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//Bucamos el producto
$product = Product::find($id);
//Si el producto no existe devolvemos error no encontrado
if (!$product) {
return response()->json([
'message' => 'Product not found.'
], 404);
}
//Si hay producto lo devolvemos
return $product;
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//Validación de datos
$data = $request->only('name', 'description', 'stock');
$validator = Validator::make($data, [
'name' => 'required|max:50|string',
'description' => 'required|max:50|string',
'stock' => 'required|numeric',
]);
//Si falla la validación error.
if ($validator->fails()) {
return response()->json(['error' => $validator->messages()], 400);
}
//Buscamos el producto
$product = Product::findOrfail($id);
//Actualizamos el producto.
$product->update([
'name' => $request->name,
'description' => $request->description,
'stock' => $request->stock,
]);
//Devolvemos los datos actualizados.
return response()->json([
'message' => 'Product updated successfully',
'data' => $product
], Response::HTTP_OK);
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Product $product
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//Buscamos el producto
$product = Product::findOrfail($id);
//Eliminamos el producto
$product->delete();
//Devolvemos la respuesta
return response()->json([
'message' => 'Product deleted successfully'
], Response::HTTP_OK);
}
}
Creación del Middleware
A continuación crearemos un Middleware que se encargará de validar el token del usuario, para ello ejecutamos el siguiente comando:
php artisan make:middleware JwtMiddlware
Nos creará el fichero app/Http/Middleware/JwtMiddleware.php
lo editamos y introducimos el siguiente código:
<?php
namespace App\Http\Middleware;
use Closure;
use JWTAuth;
use Exception;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use Illuminate\Http\Request;
class JwtMiddlware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
try {
$user = JWTAuth::parseToken()->authenticate();
} catch (Exception $e) {
if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException) {
return response()->json(['status' => 'Token is Invalid'], 401);
} else if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException) {
return response()->json(['status' => 'Token is Expired'], 401);
} else {
return response()->json(['status' => 'Authorization Token not found'], 401);
}
}
return $next($request);
}
}
A continuación añadimos los siguientes Middlewares en el fichero app/Http/Kernel.php
en la parte protected routeMiddleware.
'jwt.verify' => \App\Http\Middleware\JwtMiddlware::class,
'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken',
'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken',
Quedándonos así el código del $routeMiddleware:
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
//Custom
'jwt.verify' => \App\Http\Middleware\JwtMiddlware::class,
'jwt.auth' => 'Tymon\JWTAuth\Middleware\GetUserFromToken',
'jwt.refresh' => 'Tymon\JWTAuth\Middleware\RefreshToken',
];
Configurar el fichero de rutas
Por último, ya solo nos queda configurar las rutas de la API REST, el fichero sobre el que trabajaremos es el «routes/api.php», sustituimos el contenido por el siguiente:
<?php
use App\Http\Controllers\V1\ProductsController;
use App\Http\Controllers\V1\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::prefix('v1')->group(function () {
//Prefijo V1, todo lo que este dentro de este grupo se accedera escribiendo v1 en el navegador, es decir /api/v1/*
Route::post('login', [AuthController::class, 'authenticate']);
Route::post('register', [AuthController::class, 'register']);
Route::get('products', [ProductsController::class, 'index']);
Route::get('products/{id}', [ProductsController::class, 'show']);
Route::group(['middleware' => ['jwt.verify']], function() {
//Todo lo que este dentro de este grupo requiere verificación de usuario.
Route::post('logout', [AuthController::class, 'logout']);
Route::post('get-user', [AuthController::class, 'getUser']);
Route::post('products', [ProductsController::class, 'store']);
Route::put('products/{id}', [ProductsController::class, 'update']);
Route::delete('products/{id}', [ProductsController::class, 'destroy']);
});
});
Probando la API REST con Postman
Habiendo configurado las rutas nos quedará la siguiente estructura de nuestra API REST.
Método | Path | Autentificación |
---|---|---|
POST | api/v1/login | NO |
POST | api/v1/register | NO |
POST | api/v1/logout | SÍ |
POST | api/v1/get-user | SÍ |
GET | api/v1/products | NO |
GET | api/v1/products/{id} | NO |
POST | api/v1/products | SÍ |
PUT | api/v1/products/{id} | SÍ |
DELETE | api/v1/products/{id} | SÍ |
A continuación vamos a probar nuestra API REST mediante Postman, configura Postman como en las imágenes.
Ruta registro
Aquí puedes apreciar que al realizar el registro nos devuelve el token, guárdalo que lo necesitaremos para cuando realicemos las peticiones que requieren autentificación.
Ruta Logout
Con el token de la anterior ruta probamos el logout, esta ruta nos sirve para invalidar el token y que ya no se pueda autentificar con él.
Ruta login
Ruta get user
Esta ruta la utilizaremos para verificar que el token del usuario no haya expirado (si estuviese expirado nos devolvería un error Unauthorized) y para obtener los datos de los usuarios. Escribe el token de la ruta login (recuerda que el de la ruta de registro lo hemos borrado).
Creación de producto
El primer paso sería ir a la pestaña authorization, seleccionar Bearer token y escribir el token que validamos mediante get user.
Creamos un producto de prueba.
Actualizamos el producto
Mostramos todos los productos
Mostrar un producto
Eliminar un producto
Código fuente Laravel 8 API REST con autentificación
Puedes descargar el código fuente en este enlace.
Para acabar el artículo, si estás pensando en comprar hosting o en comprar dominio Web, déjame recomendarte estos artículos: