Adding images to our recipes

Adding images to our recipes is very easy. Change the ArticleForm to accept an image field and make it required so all of our recipes will have one. Like this:

📁 Forms/ArticleForm.ts
import { MinLength, Required } from '@Typetron/Validation'
import { Field, Form, Rules } from '@Typetron/Forms'
import { File } from '@Typetron/Storage'

export class ArticleForm extends Form {

    @Field()
    @Rules(Required, MinLength(5))
    title: string

    @Field()
    image?: File

    @Field()
    @Rules(Required)
    content: string
}

Question: Why does the type of the image field contains the string type also?

Answer: We also type-hinted the image field to string because we will use this form to update articles as well. This means we can also send the image path back to the backend instead of an Image. If an image path is sent back, it means the user didn’t change the old image, so we can skip the upload image part of our code. We will take a look at this in the next steps in the tutorial.

Now, change the add method from HomeController to save the image on the disk like so:

📁 Controllers/Http/HomeController.ts
import { Controller, Delete, Get, Put, Post } from '@Typetron/Router'
import { ArticleForm } from 'App/Forms/ArticleForm'
import { Storage } from '@Typetron/Storage'

@Controller()
export class HomeController {

    @Get()
    index() {
        return Article.get()
    }

    @Post()
    async add(form: ArticleForm, storage: Storage) {
        if (form.image) {
            form.image = await storage.save(form.image, 'public/articles')
        }
        return Article.create(form)
    }

    @Get(':Article')
    read(article: Article) {
        return article
    }

    @Put(':Article')
    update(article: Article, form: ArticleForm) {
        return article.save(form)
    }

    @Delete(':Article')
    async delete(article: Article) {
        await article.delete()
    }

}

Using the Storage class we get access to our disk and use it to store our image.

In order to test this we need to send a special kind of data to the server called form-data. You can easily make this type of request from Postman by changing the body type from raw to form-data and fill it with the data we want like in the image below:

🌐 [POST] /
{
    "title": "Cool article",
    "content": "Cool article content",
    "image": "<imageFile>"
}

If we test by making a POST request, we get back a successful result. But where is the image? If you take a look inside the public/articles directory in your project’s root folder, you will see a weird named file. That’s our image. It just got a weird name, so it won’t conflict with other files that might be in that folder, but other than that is the exact same image we’ve uploaded.

All we have to do now is to link the images with the created articles because if you try to make a GET request now, you won’t get any reference to images.

Adding images to articles

To add images to our articles we just have to add a new property in our Article entity for the image path:

📁 Entity/Article.ts
import { Column, CreatedAt, Entity, ID, Options, PrimaryColumn, UpdatedAt } from '@Typetron/Database'

@Options({
    table: 'articles',
})
export class Article extends Entity {
    @PrimaryColumn()
    id: ID

    @Column()
    title: string

    @Column()
    content: string

    @Column()
    image: string

    @CreatedAt()
    createdAt: Date

    @UpdatedAt()
    updatedAt: Date
}

After you save the file, Typetron will synchronize the newly added field with the database by running an SQL “ALTER” command.

Let’s make a new article by making again an HTTP Post request, so the field image will get populated in the database. The value for the image column will be the image’s name including the extension. Now, if we make an HTTP GET request to localhost:8000 we can see our image key with the weird name we’ve seen in the public/articles directory.

There is a small issue though. All the other articles don’t have an image yet. You can use the edit route to update their image, but before that we need to update the endpoint to upload images too. This will get a bit complicated since we also need to take into account that a user can also send back the original’s image path back to the server when editing an article. In this case we need to change the ArticleForm’s image property to be of type string | Image:

  • string in case the image value is a string with the original path
  • Image in case the image value is a media file

Since we have two type of our ArticleForm.image property, we need to change our endpoint from HomeController to handle them both:

📁 Controllers/Http/HomeController.ts
import { Controller, Delete, Get, Put, Post } from '@Typetron/Router'
import { ArticleForm } from 'App/Forms/ArticleForm'
import { Article } from 'App/Entities/Article'
import { File, Storage } from '@Typetron/Storage'

@Controller()
export class HomeController {

    @Get()
    index() {
        return Article.get()
    }

    @Post()
    async add(form: ArticleForm, storage: Storage) {
        if (form.image) {
            await storage.save(form.image, 'public/articles')
        }
        return Article.create(form)
    }

    @Get(':Article')
    read(article: Article) {
        return article
    }

    @Put(':Article')
    async update(article: Article, form: ArticleForm, storage: Storage) {
        if (form.image) {
            await storage.delete(`public/articles/${article.image}`)
            await storage.save(form.image, 'public/articles')
        }
        return article.save(form)
    }

    @Delete(':Article')
    async delete(article: Article) {
        await article.delete()
    }
}

Seeing images publicly

In order to see images in the browser, we need to activate the static assets serving feature in our app. We can easily do this by going to config/app.ts and add/uncomment the staticAssets property:

📁 config/app.ts
/* tslint:disable:no-default-export */
import { AppConfig, DatabaseProvider } from '@Typetron/Framework'
import { RoutingProvider } from 'App/Providers/RoutingProvider'
import { AppProvider } from 'App/Providers/AppProvider'

export default new AppConfig({
    port: 8000,
    environment: 'development',
    middleware: [],
    providers: [
        AppProvider,
        RoutingProvider,
        DatabaseProvider
    ],
    staticAssets: {
        '': ['public'] // <-- this
    }
})

This will allow us to see everything from our public directory. Let’s open one of our images in the browser by making an HTTP GET request to localhost:8000/articles/the-weird-image-name, eg:

🌐 [GET] /articles/upload_73830303b8e2.jpg


Question: Where does the “articles/” prefix comes from?

Answer: It comes from this line await storage.save(form.image, ‘public/articles’). Here we save our images inside the public/articles directory

Question: Why doesn’t the route look like this: localhost:8000/public/articles/image-name?

Answer: This is because the staticAssets setting is set to serve the files from the public directory already.

In the next part we will add an authentication layer to our app so only we can edit the article.

Next >

Authentication