Tutoriales de Node.js: De Cero a una API Probada

Aug 19, 2026·28 min read

Node.js lleva JavaScript fuera del navegador, pero aprenderlo exige conectar varias piezas: la terminal inicia el proceso, los módulos organizan el código, las promesas esperan la entrada y salida, HTTP recibe peticiones y las pruebas comprueban el resultado.

Este tutorial instala Node.js 24 LTS y construye una API JSON de tareas con módulos ES, async/await, persistencia en archivos, variables de entorno y pruebas mediante node:test.

Qué es Node.js y qué vas a construir

Node.js es un entorno de ejecución de JavaScript abierto y multiplataforma. Ejecuta el motor V8 fuera del navegador y añade una biblioteca estándar para trabajar con archivos, procesos, redes y servidores.

No es otro lenguaje. Tampoco es un servidor preparado que empieza a responder en cuanto lo instalas. Tú escribes el programa que abre un puerto, interpreta cada petición y decide qué respuesta enviar.

Un lenguaje, dos entornosNavegadorDOM y páginaEventosvisualesNode.jsArchivosRed yprocesosJavaScripten V8El entorno decide qué APIs existen
El mismo JavaScript se ejecuta dentro de entornos con herramientas diferentes.

El proyecto acumulativo será una API de tareas con dos operaciones:

  • GET /tasks devuelve todas las tareas.
  • POST /tasks valida una tarea nueva, la guarda y responde con el recurso creado.

La API usará únicamente herramientas incluidas en Node.js: node:http, node:fs/promises, node:test, módulos ES y la función global fetch. El repositorio terminará con configuración, persistencia, manejo de errores y pruebas automatizadas.

Node.js encaja bien cuando una aplicación pasa tiempo esperando archivos, servicios o conexiones de red. Su biblioteca proporciona operaciones asíncronas que permiten que JavaScript atienda otro trabajo mientras espera esas entradas y salidas.

Eso no vuelve rápida cualquier operación. Un cálculo intensivo ejecutado dentro de un callback sigue ocupando el event loop hasta que termina. Node.js tampoco aporta una base de datos, autenticación o escalabilidad automática.

Primero aprenderás las piezas incorporadas. Después podrás reconocer qué trabajo te ahorra un framework y qué comportamiento sigue siendo responsabilidad de tu aplicación.

Instala Node.js LTS y prepara el proyecto

El proyecto Node.js recomienda versiones Active LTS o Maintenance LTS para aplicaciones de producción. Según el calendario oficial de versiones, el 19 de agosto de 2026 Node.js 24 es la versión LTS, así que este tutorial usa esa línea sin fijar un parche concreto.

Instala la versión 24 con el instalador o gestor de versiones que corresponda a tu sistema. Node.js Tutorial: From Install to a Running Server desarrolla las opciones de instalación para cada plataforma.

Abre una terminal nueva y comprueba el ejecutable:

node --version

La salida debe empezar por v24.. Los dos números restantes dependen de la actualización instalada.

Comprueba también npm, el gestor de paquetes que se distribuye con Node.js:

npm --version

El comando imprime un número de versión. Si node funciona pero npm no existe, instala npm o usa una distribución de Node que lo incluya antes de continuar.

El REPL permite ejecutar JavaScript una expresión cada vez. Inícialo así:

node

Cuando aparezca el indicador >, escribe una operación y sal con .exit:

> 21 * 2
42
> .exit

El mismo motor también ejecuta archivos. Crea un directorio de práctica y entra en él:

mkdir tutorial-node-api
cd tutorial-node-api

Crea inicio.js con este programa:

console.log('Node ejecuta este archivo');

Ejecuta el archivo desde la terminal:

node inicio.js

Resultado esperado:

Node ejecuta este archivo

Node carga inicio.js, evalúa la llamada a console.log y termina porque ya no queda trabajo pendiente.

Ahora inicializa los metadatos del proyecto:

npm init -y

El comando crea package.json y muestra la ruta del archivo escrito. Reemplaza su contenido por la configuración canónica del proyecto:

{
  "name": "tutorial-node-api",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node --env-file=.env src/server.js",
    "test": "node --test"
  }
}

"type": "module" hace que los archivos .js del paquete se interpreten como módulos ES. El campo private evita que este proyecto de práctica se publique accidentalmente como paquete, y scripts da nombres estables a los comandos habituales.

Crea por último los directorios que usará la implementación:

mkdir src test data

inicio.js ya cumplió su propósito y no forma parte de la API final.

Del navegador a Node: CLI, módulos y npm

En el navegador, una página inicia JavaScript mediante una etiqueta de script. En Node.js, tú eliges el archivo de entrada desde la terminal con node archivo.js o mediante un script de npm.

Un módulo es un archivo que puede exportar valores e importar valores de otros archivos. Una dependencia es código que el proyecto necesita y que puede proceder de Node.js, de tu propio repositorio o de un paquete instalado.

Los equivalentes más útiles al cambiar de entorno son estos:

En el navegadorEn Node.jsQué cambia
globalThisglobalThisConserva el mismo nombre, pero contiene APIs del entorno de Node.js.
fetch(url)fetch(url)Node.js 24 lo proporciona globalmente y no necesita una importación.
import y exportimport y exportFuncionan como módulos ES cuando package.json contiene "type": "module".
Eventos de la páginaProceso y servidorNo existen window ni document; el programa responde a entradas del proceso o de la red.

No confundas globalThis con el contenido disponible dentro de él. La expresión existe en ambos entornos, pero una API del DOM como document.querySelector pertenece al navegador y no aparece en un proceso normal de Node.js.

Node.js admite CommonJS y módulos ECMAScript. CommonJS utiliza require() y module.exports; ESM utiliza import y export. Este repositorio usa ESM de principio a fin:

import { readFile } from 'node:fs/promises';

export async function readText(path) {
  return readFile(path, 'utf8');
}

El prefijo node: identifica un módulo incorporado. No se descarga desde npm y no aparece en dependencies.

Una importación propia comienza con una ruta relativa y suele incluir la extensión:

import { readTasks } from './store.js';

Una importación de paquete utiliza su nombre:

import framework from 'framework';

Esa última forma solo funcionaría después de instalar el paquete. npm install framework añadiría la dependencia a package.json y guardaría sus archivos bajo node_modules. La API de este tutorial no necesita ese paso.

Los scripts de package.json evitan recordar comandos completos. npm start ejecutará el script start, mientras que este comando ejecutará las pruebas:

npm test

npm organiza el proyecto y sus dependencias; no reemplaza a Node.js. Al ejecutar un script, npm termina invocando los programas declarados en package.json.

Async/await y el event loop sin mitos

Una operación bloqueante impide que el event loop avance mientras espera o calcula un resultado. Si JavaScript recorre datos durante varios segundos sin ceder el control, ninguna otra función JavaScript del mismo event loop puede ejecutarse durante ese intervalo.

Una promesa representa un resultado que todavía no está disponible. await pausa la función async que contiene la expresión hasta que la promesa se resuelve o se rechaza; no detiene por sí mismo todo el proceso.

La diferencia aparece al leer un archivo:

import { readFile } from 'node:fs/promises';

async function showConfiguration() {
  const text = await readFile('package.json', 'utf8');
  console.log(text.trim());
}

await showConfiguration();

readFile devuelve una promesa. Mientras la operación de archivo está pendiente, la función showConfiguration no continúa después de await, pero Node.js puede atender otros eventos.

Qué ocurre al llegar a awaitFunción asyncawait readFile(…)queda pausadaEvent looppuede atenderotros eventosWorker poolrealiza la lecturadelega E/Spromesa listareanudaSolo la continuación espera el resultado
await pausa una función, no todo el proceso de Node.js.

node:fs/promises utiliza el thread pool subyacente para sus operaciones asíncronas de archivos. Cuando la lectura termina, la promesa queda lista y la continuación de la función vuelve a ejecutarse como JavaScript.

El event loop ejecuta callbacks de JavaScript y coordina la entrada y salida no bloqueante. El worker pool ejecuta determinadas tareas costosas o bloqueantes implementadas mediante código nativo.

El recorrido de una petición puede representarse así:

petición HTTP
  → callback JavaScript (event loop)
  → operación asíncrona
      → E/S del sistema o worker pool
  → promesa resuelta
  → continuación JavaScript (event loop)
  → respuesta HTTP

Node.js no es completamente monohilo. El JavaScript de este servidor se ejecuta mediante el event loop, mientras algunas operaciones incorporadas pueden usar otros hilos. Son afirmaciones distintas.

async/await tampoco transforma un cálculo síncrono en trabajo asíncrono:

async function calculate() {
  let total = 0;

  for (let index = 0; index < 5_000_000; index += 1) {
    total += index;
  }

  return total;
}

Aunque calculate devuelve una promesa, el bucle se ejecuta de forma síncrona cuando llamas a la función. La palabra async cambia el valor devuelto y permite usar await; no mueve el cuerpo a otro hilo.

El event loop: microtasks and macrotasks desarrolla el orden de callbacks y promesas en el navegador. Node.js añade sus propias fuentes de trabajo, pero conserva la idea central: el código JavaScript actual debe terminar antes de que otro callback pueda ejecutarse.

Tutorial: crea una API JSON con HTTP y archivos

La implementación final tendrá tres archivos de aplicación. store.js se ocupa del disco, app.js procesa HTTP y server.js carga la configuración e inicia el proceso.

Quién hace quéClienteHTTPserver.jsconfigura e iniciaapp.jsdecide larespuestastore.jslee y guardatasks.jsonen discodatosrespuesta
Cada archivo protege un límite distinto de la aplicación.

Empieza con la persistencia completa en src/store.js:

import { readFile, writeFile } from 'node:fs/promises';

export async function readTasks(file) {
  try {
    const text = await readFile(file, 'utf8');
    const tasks = JSON.parse(text);

    if (!Array.isArray(tasks)) {
      throw new Error('Task file must contain an array');
    }

    return tasks;
  } catch (error) {
    if (error.code === 'ENOENT') {
      return [];
    }

    throw error;
  }
}

export async function saveTasks(file, tasks) {
  const text = `${JSON.stringify(tasks, null, 2)}\n`;
  await writeFile(file, text, 'utf8');
}

La primera lectura devuelve [] cuando el archivo todavía no existe. Un archivo existente pero dañado no se trata como una lista vacía, porque hacerlo borraría el rastro del problema durante la siguiente escritura.

Comprueba de forma aislada la lectura y la escritura:

node --input-type=module --eval "import { rm } from 'node:fs/promises'; import { readTasks, saveTasks } from './src/store.js'; const file = 'store-check.json'; await rm(file, { force: true }); console.log(JSON.stringify(await readTasks(file))); await saveTasks(file, [{ id: 1 }]); console.log(JSON.stringify(await readTasks(file))); await rm(file);"

Resultado esperado:

[]
[{"id":1}]

Ahora crea src/app.js con el servidor HTTP completo:

import { createServer } from 'node:http';
import { readTasks, saveTasks } from './store.js';

class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.status = status;
  }
}

function sendJson(response, status, body) {
  response.writeHead(status, {
    'content-type': 'application/json; charset=utf-8',
  });
  response.end(JSON.stringify(body));
}

async function readJson(request) {
  const contentType = request.headers['content-type'] ?? '';

  if (contentType.split(';', 1)[0].trim().toLowerCase() !== 'application/json') {
    throw new HttpError(400, 'Content-Type must be application/json');
  }

  const chunks = [];

  // El cuerpo HTTP llega como un stream de fragmentos binarios.
  for await (const chunk of request) {
    chunks.push(chunk);
  }

  try {
    // Buffer reúne los fragmentos antes de decodificarlos como texto UTF-8.
    return JSON.parse(Buffer.concat(chunks).toString('utf8'));
  } catch {
    throw new HttpError(400, 'Body must contain valid JSON');
  }
}

export function createApp({ dataFile }) {
  return createServer(async (request, response) => {
    try {
      const url = new URL(request.url, 'http://localhost');

      if (request.method === 'GET' && url.pathname === '/tasks') {
        const tasks = await readTasks(dataFile);
        sendJson(response, 200, tasks);
        return;
      }

      if (request.method === 'POST' && url.pathname === '/tasks') {
        const input = await readJson(request);

        if (
          input === null
          || typeof input !== 'object'
          || Array.isArray(input)
          || typeof input.title !== 'string'
          || input.title.trim() === ''
        ) {
          throw new HttpError(400, 'title must be a non-empty string');
        }

        const tasks = await readTasks(dataFile);
        const nextId = tasks.reduce(
          (highest, task) => Math.max(highest, task.id),
          0,
        ) + 1;

        const task = {
          id: nextId,
          title: input.title.trim(),
          done: false,
        };

        tasks.push(task);
        await saveTasks(dataFile, tasks);
        sendJson(response, 201, task);
        return;
      }

      throw new HttpError(404, 'Route not found');
    } catch (error) {
      if (error instanceof HttpError) {
        sendJson(response, error.status, { error: error.message });
        return;
      }

      console.error(error);
      sendJson(response, 500, { error: 'Internal server error' });
    }
  });
}

createServer recibe una función para cada petición. La URL separa la ruta de cualquier consulta, request.method distingue GET de POST, y sendJson mantiene juntos el código de estado, la cabecera y el cuerpo JSON.

Cómo se elige una rutaLlega una petición¿Método y rutacoinciden?GET /tasksleer · 200POST /taskscrear · 201Otracombinaciónno existe · 404GETPOSTninguna
La combinación de método y ruta decide qué rama atiende la petición.

readJson exige la cabecera Content-Type, reúne los fragmentos del cuerpo y convierte el texto con JSON.parse. Un JSON mal formado y un título vacío producen 400. Una ruta desconocida produce 404; un fallo inesperado al leer o escribir produce 500 sin enviar detalles internos.

El cuerpo llega por partes{“title”:“leer”}fragmentosBuffer.concatune bytestexto UTF-8{“title”:“leer”}Objeto JStitle: leerJSON.parseSolo al final existe un valor que validar
Del stream HTTP fragmentado a un objeto JavaScript validable.

El estado 200 acompaña una lectura correcta. 201 indica que POST creó y guardó una tarea nueva.

Esta persistencia es educativa. Una lectura seguida de una escritura no protege contra dos peticiones concurrentes, y una puede sobrescribir el resultado de la otra. Usa una base de datos antes de llevar este diseño a producción.

Cómo se pierde una escrituraPetición Aquiere añadir APetición Bquiere añadir BAmbas leen[]A escribe primero[A]B escribe después[B]sobrescribeResultado final: [B] — A desapareció
Dos ciclos de lectura y escritura pueden perder una actualización.

Comprueba createApp antes de añadir el punto de entrada:

node --input-type=module --eval "import { once } from 'node:events'; import { createApp } from './src/app.js'; const server = createApp({ dataFile: 'app-check.json' }); server.listen(0, '127.0.0.1'); await once(server, 'listening'); const { port } = server.address(); const response = await fetch('http://127.0.0.1:' + port + '/tasks'); console.log(response.status, await response.text()); await new Promise((resolve) => server.close(resolve));"

Resultado esperado:

200 []

Completa el punto de entrada en src/server.js:

import { resolve } from 'node:path';
import { createApp } from './app.js';

const port = Number(process.env.PORT ?? 3000);

if (!Number.isInteger(port) || port <= 0 || port > 65535) {
  console.error('PORT must be an integer between 1 and 65535');
  process.exitCode = 1;
} else {
  const server = createApp({
    dataFile: resolve('data/tasks.json'),
  });

  server.on('error', () => {
    console.error('Server could not start');
    process.exitCode = 1;
  });

  server.listen(port, '127.0.0.1', () => {
    console.log(`API listening on http://127.0.0.1:${port}`);
  });
}

Antes de iniciar el servidor, crea .env en la raíz del proyecto:

PORT=3000

Ejecuta el script:

npm start

Si PORT no está definida ya en el entorno, terminará mostrando esta línea:

API listening on http://127.0.0.1:3000

Deja ese proceso abierto y utiliza otra terminal para comprobar la lista inicial:

curl -i http://127.0.0.1:3000/tasks

La respuesta tiene estado 200, cabecera JSON y cuerpo [].

Crea una tarea con una petición completa:

curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"probar la API"}' \
  http://127.0.0.1:3000/tasks

El estado es 201 y el cuerpo contiene el recurso creado:

{"id":1,"title":"probar la API","done":false}

Vuelve a leer la colección:

curl http://127.0.0.1:3000/tasks
[{"id":1,"title":"probar la API","done":false}]

Prueba también una entrada incorrecta:

curl -i \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"   "}' \
  http://127.0.0.1:3000/tasks

El servidor responde con 400 y este cuerpo:

{"error":"title must be a non-empty string"}

En conjunto, estas comprobaciones cubren el inicio del servidor, la lectura, la creación, la persistencia y la validación. Si una falla, revisa las piezas que intervienen en ese recorrido.

Configura y maneja los errores

Un error operacional procede de una condición que el programa espera encontrar: una ruta desconocida, JSON inválido, datos que no pasan la validación o un archivo inaccesible.

Un error de programación indica un defecto del código, como llamar a una función inexistente o usar una variable equivocada. La petición recibe 500 en ambos casos inesperados, pero el registro interno conserva el error para que puedas investigarlo.

El archivo .env que creaste en la raíz contiene el puerto predeterminado. El script start ya contiene node --env-file=.env src/server.js. La opción --env-file carga los pares del archivo en process.env, por lo que server.js puede leer process.env.PORT.

El operador ?? proporciona 3000 cuando la variable no existe. La conversión y la validación impiden iniciar el servidor con un puerto vacío, decimal o negativo.

Puedes comprobar otra configuración sin cambiar el código. En macOS o Linux:

PORT=4100 node src/server.js

En PowerShell:

$env:PORT=4100; node src/server.js

En cmd.exe:

set PORT=4100&& node src/server.js

Resultado esperado:

API listening on http://127.0.0.1:4100

En esos comandos, la variable pertenece al proceso iniciado desde la terminal. Con npm start, .env aporta el valor solo si PORT no está definida ya en el entorno.

La función sendJson centraliza las respuestas, así que todos los errores públicos conservan el mismo formato. HttpError guarda los códigos controlados 400 y 404. Los demás errores se registran con console.error y se convierten en un mensaje 500 genérico.

Nunca devuelvas el objeto de error inesperado al cliente. Puede contener rutas locales, fragmentos de datos o detalles que solo sirven durante la depuración.

La separación también mantiene una regla estable: la entrada del cliente determina los errores 400, una ruta ausente determina 404, y un fallo interno determina 500.

Dónde nace cada errorEntradaJSON inválidoo título vacío400RutaLa combinaciónno existe404InteriorFallo inesperadodel servidor500Cliente → ruta → funcionamiento interno
El origen del problema determina la familia de respuesta de error.

Prueba, depura y continúa aprendiendo

Las pruebas automatizadas deben llamar a la misma createApp que usa server.js. Así comprueban las rutas reales sin iniciar un segundo proceso ni escribir en data/tasks.json.

La prueba usa la aplicación realserver.jsuso normalapp.test.jspeticiones depruebacreateApplas mismas rutas realesdata/tasks.jsondirectorio temporalse eliminadataFile cambia; la app no
Una sola aplicación, dos configuraciones de almacenamiento aisladas.

Crea test/app.test.js con la prueba completa:

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createApp } from '../src/app.js';

test('la API valida, crea y devuelve tareas', async () => {
  const directory = await mkdtemp(join(tmpdir(), 'node-tasks-'));
  const dataFile = join(directory, 'tasks.json');
  const server = createApp({ dataFile });

  await new Promise((resolve) => {
    server.listen(0, '127.0.0.1', resolve);
  });

  const address = server.address();
  const baseUrl = `http://127.0.0.1:${address.port}`;

  try {
    const emptyResponse = await fetch(`${baseUrl}/tasks`);
    assert.equal(emptyResponse.status, 200);
    assert.deepEqual(await emptyResponse.json(), []);

    const invalidResponse = await fetch(`${baseUrl}/tasks`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ title: '   ' }),
    });
    assert.equal(invalidResponse.status, 400);
    assert.deepEqual(await invalidResponse.json(), {
      error: 'title must be a non-empty string',
    });

    const createdResponse = await fetch(`${baseUrl}/tasks`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ title: 'escribir pruebas' }),
    });
    assert.equal(createdResponse.status, 201);
    assert.deepEqual(await createdResponse.json(), {
      id: 1,
      title: 'escribir pruebas',
      done: false,
    });

    const listResponse = await fetch(`${baseUrl}/tasks`);
    assert.deepEqual(await listResponse.json(), [
      {
        id: 1,
        title: 'escribir pruebas',
        done: false,
      },
    ]);

    const missingResponse = await fetch(`${baseUrl}/missing`);
    assert.equal(missingResponse.status, 404);
    assert.deepEqual(await missingResponse.json(), {
      error: 'Route not found',
    });
  } finally {
    await new Promise((resolve, reject) => {
      server.close((error) => {
        if (error) reject(error);
        else resolve();
      });
    });

    await rm(directory, { recursive: true });
  }
});

La prueba abre el servidor en un puerto disponible, usa un directorio temporal y elimina ese directorio al terminar. Las cinco comprobaciones cubren 200, 201, 400 y 404, además de demostrar que una tarea creada reaparece en la lectura siguiente.

Ejecuta el conjunto con el script de npm:

npm test

node --test descubre test/app.test.js. El resumen final debe indicar una prueba aprobada y cero fallos; las líneas de duración cambian entre ejecuciones.

El módulo node:test y su ejecutor forman parte de Node.js, igual que node:assert/strict. Testing: Vitest & Playwright muestra cuándo ampliar esta base con herramientas para aplicaciones mayores y pruebas dentro del navegador.

Para depurar un fallo, inicia el proceso con el inspector:

node --inspect --env-file=.env src/server.js

La terminal muestra una dirección del inspector y después el registro de inicio de la API. Puedes conectar las herramientas de desarrollo, colocar puntos de interrupción y seguir los valores de request.method, url.pathname y tasks.

Los registros también sirven para localizar el límite roto. Registra el método, la ruta y un identificador de petición cuando el proyecto crezca, pero evita imprimir cuerpos, secretos o el contenido completo de .env.

Antes de ampliar el proyecto, comprueba la base:

  • node --version comienza por v24..
  • npm start abre la API en el puerto configurado.
  • GET /tasks devuelve un array y estado 200.
  • POST /tasks devuelve 201 y persiste la tarea.
  • Las entradas incorrectas devuelven 400.
  • Las rutas desconocidas devuelven 404.
  • Los errores internos devuelven 500 sin filtrar detalles.
  • npm test termina sin fallos.

Express puede reducir el código dedicado a rutas, cuerpos y middleware. Una base de datos puede reemplazar el archivo JSON y coordinar escrituras concurrentes. Deploying a JavaScript App cubre el paso desde el proceso local hasta un entorno desplegado.

Esas son ampliaciones, no requisitos para entender el servidor que ya funciona. Cuando quieras seguir el recorrido dentro del currículo, Server-Side JavaScript conecta Node.js con rutas, persistencia, autenticación y despliegue.

Frequently asked questions

¿Qué necesito saber antes de aprender Node.js?
Necesitas conocer variables, funciones, objetos y promesas básicas de JavaScript. No necesitas experiencia con servidores, bases de datos ni frameworks para completar este tutorial.
¿Qué versión de Node.js debo instalar?
Instala Node.js 24 LTS y mantenlo actualizado dentro de esa versión principal. El proyecto Node.js recomienda usar versiones Active LTS o Maintenance LTS para aplicaciones de producción.
¿Node.js utiliza un solo hilo?
El event loop ejecuta los callbacks de JavaScript, pero eso no significa que todo el proceso dependa de un único hilo. Node.js también puede delegar determinadas operaciones nativas, como algunas tareas de archivos, a un worker pool.
¿Necesito Express para crear una API con Node.js?
No. El módulo incorporado node:http permite crear servidores, examinar métodos y rutas, leer cuerpos y devolver JSON sin instalar dependencias. Express puede añadirse después para reducir código repetitivo.
¿Puedo usar archivos JSON como base de datos?
Sirven para aprender y para herramientas locales con una sola escritura cada vez. No son una persistencia adecuada para producción ni para peticiones concurrentes, porque dos escrituras pueden sobrescribirse.