Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Crear proyecto Backend con TypeScript

Contenidos
  1. Iniciando un proyecto Backend
    1. Instalación y correr proyecto
    2. Rutas para administrar información
    3. Agregar TypeScript al proyecto existente
    4. Typado de Express y otros
    5. Sequelize y TypeScript
  2. Creando un Modelo con interfaz TypeScript
  3. Configuración Jest para pruebas con TypeScript
  4. Configuración EsLint para linter con TypeScript

Iniciando un proyecto Backend

Crear un servidor para exponer operaciones CRUD sobre un recurso con buenas prácticas utlizando TypeScript

👀 Instrucciones 👀

Pre-requisitos

  • Postman
  • Visor Local de Base de datos SQLite.

Sobre el último punto recomendamos:

  • DB Browser for SQLite
  • SQLite Viewer (por Florian Klampfer) para Visual Studio Code

Si no te acomodan las opciones anteriores procura utilizar una alternativa robusta y con buena calificación de la comunidad.

Instalación y correr proyecto

  • npm install
  • npm run dev

Rutas para administrar información

  • analizar que pasa al entrar a http://localhost:3000/api/v1/artists
  • crear una lista de artistas con id, name, description, code y image, y aplicar las operaciones sobre la lista en cada ruta

Hasta acá las instrucciones parecerán similares a lo visto en el capítulo de JavaScript en el Servidor pero a diferencia de ese capítulo en esta ocasión buscaremos un poco más en detalle en la documentación de Sequelize para averiguar sobre como manejar todo lo referente a nuestra base de datos. Esto nos llevará a encontrarnos con la necesidad de instalar unos paquetes adicionales.

Agregar TypeScript al proyecto existente

En el capítulo de Setup de TypeScript aprendimos los paquetes más importantes para comenzar con TypeScript. Los instalaremos como sigue:

  npm install -D typescript ts-node

Agregaremos una configuración de TypeScript de forma automática utilizando el siguiente comando:

  npx tsc --init 

Ahora modificaremos el archivo nodemon.json para que reconozca los archivos con extensión “.ts”

{
  "env": {
    ...
  },
  "execMap": {
    "ts": "ts-node"
  }
}

y luego el script “dev” del archivo package.json

 "dev": "nodemon src/server.ts",

Nodemon ahora está configurado para ejecutar un comando personalizado para archivos TypeScript. Cuando llama a nodemon con un archivo TypeScript (es decir, nodemon src/server.ts), nodemon encontrará el comando en execMap que se correlaciona con los archivos .ts y luego ejecutará ese comando, pasando el archivo como argumento final.

Nuestro proyecto actualmente no tiene archivo con extensión “.ts”. Por el momento solo cambiaremos la extensión del archivo src/server.js a src/server.ts

Ahora correremos el comando para iniciar Nodemon:

npm run dev

Obtendremos una serie de errores de TypeScript lo que nos indicará que ya estamos procesando los archivos sin problema.

Typado de Express y otros

Para agregar tipado a un proyecto TypeScript necesitamos indicar que tenemos los tipos de datos bien representados para Express. Para ello ya existe una solución llamada @types/express. Además durante el desarrollo con Express estaremos interactuando con otros módulos importante que necesitaremos agregar sus tipos

npm i -D @types/express @types/node @types/cookie-parser @types/express

Deberemos cambiar nuestros archivos app.ts y server.ts para que se vea de la siguiente forma:

src/app.ts

import express, { Express, Request, Response }  from 'express'
import path from 'path'
import cookieParser from 'cookie-parser'
import router from './routes'

const app: Express = express()

// GESTIÓN COOKIES
app.use(cookieParser())

app.get('/', (req: Request, res: Response) => {
  res.sendFile(
    path.resolve('public/index.html')
  )
})

app.get('/bio', (req: Request, res: Response) => {
  res.sendFile(
    path.resolve('public/biography.html')
  )
})

app.get('/app.js', (req: Request, res: Response) => {
  res.sendFile(
    path.resolve('public/assets/app.js')
  )
})

// GESTIÓN INFORMACIÓN
app.use(express.json())
app.use(router)

export default app

src/server.ts

import app from './app'

// LEVANTAR EL SERVIDOR
const port = process.env.PORT;
const environment = process.env.NODE_ENV

app.listen(port, () => {
  console.log(`App server listening on port ${port} in ${environment} environment`)
})


Notarás que ahora todas las sentencias de importación de otros módulos esta vez es con la sintáxis de TypeScript. Si tu editor está integrado correctamente con TypeScript puede posar tu cursor sobre las variables de los callback de express para ver información relevante. Esto ya es un primer paso para poder aprovechar al máximo las capacidades de documentación de TypeScript.

Si ejecutamos nuevamente npm run dev notaremos un error como sigue:

Could not find a declaration file for module './routes'

Esto es porque nuestro archivo no tiene la extensión indicada. En esta parte migraremos todos los archivos existentes a la extensión “.ts”. Su contenido lo modificaremos utilizando la sintáxis de importación correspondiente.

src/routes/index.ts

import express, { Router }  from 'express'
import artistsController from '../controllers/artistsController'

const router: Router = express.Router()

router.get('/api/v1/artists', artistsController.getAllArtists)
router.post('/api/v1/artists', artistsController.saveArtist)
router.put('/api/v1/artists/:id', artistsController.updateArtist)
router.delete('/api/v1/artists/:id', artistsController.removeArtist)

export default router

src/routes/artistsController.ts

import { Request, Response }  from 'express'

export default {
  getAllArtists: (request: Request, response: Response) => {
    response
      .status(200)
      .json([])
  },
  saveArtist: (request: Request, response: Response) => {},
  updateArtist: (request: Request, response: Response) => {},
  removeArtist: (request: Request, response: Response) => {}
}

Una vez ejecutados todos estos cambios veremos que el comando npm run dev correrá sin problemas.

Sequelize y TypeScript

  • Instalaremos Sequelize con un serie de comandos:
  npm install --save-dev @types/validator @types/sequelize sqlite3
  npm install sequelize reflect-metadata sequelize-typescript

más detalle del porque necesitamos esta configuración en este enlace

  • crear en la raíz el archivo .sequelizerc con el siguiente contenido:
const path = require('path')

module.exports = {
 'config': path.resolve('./src/config', 'config.json'),
 'models-path': path.resolve('./src/models'),
 'seeders-path': path.resolve('./src/seeders'),
'migrations-path': path.resolve('./src/migrations')
}

Nota que este ultimo archivo se mantendrá con código JavaScript (NodeJS) y no TypeScript

  • Crearemos un archivo src/config/config.json
{
  "development": {
    "username": "root",
    "password": null,
    "database": "database_development",
    "host": "local.database.sqlite3",
    "dialect": "sqlite"
  }
}

  • Agregamos a tsconfig.json las siguientes opciones
 "experimentalDecorators": true,
 "emitDecoratorMetadata": true,              
  • Crearemos un archivo src/sequelize.ts
import path from 'path'
import { Sequelize } from 'sequelize-typescript'

const envOptions = {
  DATABASE_URL: process.env.DATABASE_URL ?? '',
  NODE_ENV: process.env.NODE_ENV
}
const env = envOptions.NODE_ENV ?? 'development'
const baseConfig = require(path.join(__dirname, '/config/config.json'))[env]
const config = {
  ...baseConfig,
  models: [path.join(__dirname, '/models')]
}
const sequelize = envOptions.DATABASE_URL !== ''
  ? new Sequelize(envOptions.DATABASE_URL, config)
  : new Sequelize(config.database, config.username, config.password, config)

export { Sequelize, sequelize }

Notar que la configuración respecto a un proyecto creado con sequelize-cli, este quedó mucho más simplificado. Si bien deberemos hacer algunas cosas de forma manual, las ventajas del tipado nos ayudarán a escribir código consistente con los que queremos describir para la base de datos.

Creando un Modelo con interfaz TypeScript

  • Vamos a crear nuestro primer modelo pero esta vez utilizando TypeScript para describirlo. Haremos un modelo llamado Artist y crearemos un archivo en la carpera src/models llamado Artist.ts

src/models/Artist.ts

import { DataType, Model, Table, Column } from 'sequelize-typescript'
import { Sequelize } from '../sequelize'

@Table({
  timestamps: true,
  tableName: "Artists"
})
export class Artist extends Model {
  @Column({
    type: DataType.STRING,
    allowNull: false
  })
  name!: string

  @Column({
    type: DataType.STRING,
    allowNull: false
  })
  description!: string

  @Column({
    type: DataType.STRING,
    allowNull: true
  })
  code!: boolean

  @Column({
    type: DataType.STRING,
    allowNull: true
  })
  image!: boolean

  @Column({
    type: DataType.DATE,
    allowNull: true,
    defaultValue: Sequelize.literal("CURRENT_TIMESTAMP")
  })
  createdAt!: boolean

  @Column({
    type: DataType.DATE,
    allowNull: true,
    defaultValue: Sequelize.literal("CURRENT_TIMESTAMP")
  })
  updatedAt!: boolean
}

Fijate como varias de las cosas que se definen utilizando símbolos como @Table o @Column. Esto es una sintáxis especial llamada “decoradores” que se encuentra en borrador en el TC39. más detalles acá Esto ya está disponible en otros lenguajes por ejemplo Java y es común ver esto en FrameWorks como Spring.

  • Finalmente debemos modificar nuestro archivo src/server.ts para que anyes de iniciarse, realice una sincronización con la base de datos. Para esto utilizaremos el operador void para indicar al compilador que se permiten Promesas sin resolver (debido a que no tenemos un bloque try/catch a propósito):

src/server.ts

import app from './app'
import { sequelize } from './sequelize'

async function runServer (): Promise<void> {
  const port = process.env.PORT ?? 3000
  const environment = process.env.NODE_ENV ?? 'development'

  await sequelize.sync()

  app.listen(port, () => { console.log(`App server listening on port ${port} in ${environment} environment`) })
}

void runServer()

  • Por último agrega lo siguente al inicio del archivo src/controllers/artistsController.ts
import { sequelize } from '../sequelize';

const { Artist } = sequelize.models

export default {
  getAllArtists: async (request: Request, response: Response) => {
  ...
  • Será tu misión completar todos los endpoints y poniendo atención a la información proporcionada por TypeScript.

Con esta forma de trabajo solo nos centraremos en trabajar en los modelos y entender como se hace su definición. En la versión 7 se Sequelize se promete un soporte más extendido para TypeScript y a través del CLI. Hasta la versión 6, esto no es posible por lo que perderemos el sistema de migraciones.

Toda la información oficial sobre esto cambios la puedes encontrar acá:

Sequelize-TypeScript

Configuración Jest para pruebas con TypeScript

Vamos a continuar con el proyecto del capítulo anterior y ahora nos centraremos en la carpeta tests.

Lo primero será configurar Jest para utilizar TypeScript tal como lo señala su Documentación oficial de Jest para TypeScript

npm install ts-jest @types/jest @types/supertest 
npx ts-jest config:init

Agregaremos al src/config/config.json la configuración para test:

...

"test": {
  "username": "root",
  "password": null,
  "database": "database_test",
  "host": "test.database.sqlite3",
  "dialect": "sqlite",
  "logging": false
}
...

y Ahora cambiaremos la extensión del test existente:

tests/artists.test.ts

import { agent as supertest } from 'supertest'
import app from '../src/app'
import { sequelize } from '../src/sequelize'

// Crea tu propios fixtures con tus artistas favoritos
const artistsFixtures = [
  {
    id: 1,
    name: 'Luis Alberto Spinetta',
    description: 'Guitarrista, Cantante y Compositor Argentino considerado como uno de los más innovadores debido a su uso de elementos del Jazz en música popular',
    code: 'L_A_SPINETTA_GUITAR',
    image: 'https://res.cloudinary.com/boolean-spa/image/upload/v1627536010/boolean-fullstack-js/L_A_SPINETTA_GUITAR_jpg_dwboej.png'
  },
  {
    id: 2,
    name: 'Chet Baker',
    description: 'Fue un trompetista, cantante y músico de jazz estadounidense. Exponente del estilo cool. Baker fue apodado popularmente como el James Dean del jazz dado a su aspecto bien parecido',
    code: 'CHET_BAKER_TRUMPET',
    image: 'https://res.cloudinary.com/boolean-spa/image/upload/v1627536010/boolean-fullstack-js/CHET_BAKER_TRUMPET_ssbyt5.jpg'
  },
  {
    id: 3,
    name: 'Tom Misch',
    description: 'Thomas Abraham Misch es un músico y productor inglés. Comenzó a lanzar música en SoundCloud en 2012 y lanzó su álbum de estudio debut Geography en 2018',
    code: 'TOM_MISCH_GUITAR',
    image: 'https://res.cloudinary.com/boolean-spa/image/upload/v1627536009/boolean-fullstack-js/TOM_MISCH_GUITAR_r456nt.jpg'
  }
]

describe('/api/artists', () =>{
  const { Artist } = sequelize.models

  // Configurar que pasara al inicio de la batería de pruebas y al finalizar
  beforeEach(async () => {
    // Creamos la base de datos vacía con las tablas necesarias
    await sequelize.sync({ force: true })
  })

  afterAll(async () => {
    // En toda base de datos es importante cerrar la conexión
    await sequelize.close()
  })

  it('returns an list of artists', async () => {
    await Artist.bulkCreate(artistsFixtures)
    
    const response = await supertest(app)
      .get('/api/v1/artists')
      .expect(200)
    expect(response.body).toMatchObject(artistsFixtures)
  })

  it('returns 500 when the database throws error', async () => {
    // Podemos ser ingeniosos y generar un escenario que haga fallar a la base de datos
    await Artist.drop()

    const response = await supertest(app)
      .get('/api/v1/artists')
      .expect(500)
    expect(response.body).toMatchObject({ message: 'SQLITE_ERROR: no such table: Artists' })
  })

  it('create an artist', async () => {
    await Artist.bulkCreate(artistsFixtures)
    const newArtist = {
      name: "Herbie Hancock",
      description: "Es un pianista, tecladista y compositor estadounidense de jazz. A excepción del free jazz, ha tocado prácticamente todos los estilos jazzísticos surgidos tras el bebop: hard bop, fusión, jazz modal, jazz funk, jazz electrónico, etc.",
      code: "HERBIE_HANCOCK_KEYBOARD",
      image: "https://res.cloudinary.com/boolean-spa/image/upload/v1627537633/boolean-fullstack-js/HERBIE_HANCOCK_KEYBOARD_n6b2fv.jpg"
    }

    const response = await supertest(app)
      .post('/api/v1/artists')
      .send(newArtist)
      .expect(201)

    const artists: any[] = await Artist.findAll({ raw: true })
    expect(artists).toHaveLength(artistsFixtures.length + 1)
    expect(response.body.id).toEqual(artists[artists.length - 1].id)
  })

  it('get error if POST if sent without valid data', async () => {

    await Artist.bulkCreate(artistsFixtures)
    const newArtist = {
      name: "Herbie Hancock",
    }

    const response = await supertest(app)
      .post('/api/v1/artists')
      .send(newArtist)
      .expect(422)

    expect(response.body.message).toContain('notNull Violation')
  })

})

fijate como cambiaron levente las formas en que se hacen la sincronización de la base de datos debido a que el objeto que exportamos esta vez para configurar la base de datos cambio su forma (src/sequelize.ts) que anteriormente era un archivo auto-generado por el CLI que estaba en el archivo (src/models/index.js)

Configuración EsLint para linter con TypeScript

cuando se configura EsLint se necesita crear el archivo .eslintrc.json. Generalmente lo hacemos con el siguiente comando:

  npx eslint --init

Si queremos hacer esto para TypeScript debemos contestar las siguientes preguntas:

How would you like to use ESLint? To check syntax, find problems, and enforce code style


What type of modules does your project use? JavaScript modules (import/export)


Does your project use TypeScript? Yes

Where does your code run? (utilizar la barra espaciadora)

  • ✔ Node

How would you like to define a style for your project? Use a popular style guide


Which style guide do you want to follow Standard: https://github.com/standard/standard


What format do you want your config file to be in? JSON


una vez se haya creado el archivo de configuración debemos indicar donde está nuestro archivo tsconfig.json. Modificaremos la sección “parserOptions” y agregaremos la llave “project” como indica el siguiente trozo de código:

.eslintrc.json

...
"parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module",
    "project": ["tsconfig.json"]
},

...

Ahora vamos a agregar al package.json un script llamado “lint” con lo siguiente:

...
"scripts": {
  ...
  "lint": "eslint ."
  ...
}
...

y por último ejecutamos

  npm run lint

Veremos varios errores y solucionaremos algunos para revisar las reglas. Podemos ejecutar npx eslint . --fix para solucionar la mayoría de los errores.