Nextjs Refine REST API (Backend)

Nextjs Refine REST API (Backend)

Table of Contents

In previous post I have consumed the REST API for the frontend but now I want create the backend so that I get understanding of the backend it requires for Refine frontend.

API Provider Simple REST

The Simple REST data provider is a package that provides an implementation for working with REST APIs that conform to a standard API design. It is built on the foundation of the json-server package.

The Standard API Design.

This is the mapping of functions in Data provider to REST

alt text

HTTP Methods used by these functions: alt text

GET    /posts
GET    /posts/:id
POST   /posts
PUT    /posts/:id
PATCH  /posts/:id
DELETE /posts/:id
  • app/api/posts/route.ts
    • GET
    • POST
  • app/api/posts/[id]/route.ts
    • GET
    • PUT
    • PATCH
    • DELETE

Params

Range

  • start
  • end
  • limit
GET /posts?_start=10&_end=20
GET /posts?_start=10&_limit=10

Paginate

  • page
  • per_page (default = 10)
GET /posts?_page=1&_per_page=25

Sort

  • _sort=f1,f2
GET /posts?_sort=id,-views

Conditions

  • ==
  • lt<
  • lte<=
  • gt>
  • gte>=
  • ne!=
GET /posts?views_gt=9000

Embed

GET /posts?_embed=comments
GET /comments?_embed=post

Delete

DELETE /posts/1
DELETE /posts/1?_dependent=comments

Prisma integration:

Here is my blog post for this.

yarn add  prisma -D

Add all the things added in blog post

npx prisma migrate dev --name init

create a file src/lib/db.ts

// src/lib/db.ts
import { PrismaClient } from "@prisma/client";

declare global {
  var prisma: PrismaClient | undefined;
}

const prisma =
  globalThis.prisma ||
  new PrismaClient({
    // Add data proxy configuration if needed
    datasources: {
      db: {
        url: process.env.DATABASE_URL,
      },
    },
  });

if (process.env.NODE_ENV !== "production") {
  globalThis.prisma = prisma;
}

export default prisma;

Main task is to export prisma from here.

Create Backend API routes

  • app/api/[tableName]/route.ts
  • app/api/[tableName]/[id]/route.ts

This is for generic

alt text

Frontend routes

src/app/distros
├── create
│   └── page.tsx
├── edit
│   └── [id]
│       └── page.tsx
├── layout.tsx
├── page.tsx
└── show
    └── [id]
        └── page.tsx

I did all the same but you need to study about Antd


Creating Distro with an image upload:

This is the blog where I found out about the image upload.

Similar to this: https://api.fake-rest.refine.dev/media/upload

I need an API to media upload.

This end-point should be Content-type: multipart/form-data and Form Data: file: binary.

And response should be like:

{
  "url": "https://example.com/uploaded-file.jpeg"
}

This data is sent to the API when the form is submitted. [POST] https://api.fake-rest.refine.dev/posts

{
  "title": "Test",
  "image": [
    {
      "uid": "rc-upload-1620630541327-7",
      "name": "greg-bulla-6RD0mcpY8f8-unsplash.jpg",
      "url": "https://refine.ams3.digitaloceanspaces.com/78c82c0b2203e670d77372f4c20fc0e2",
      "type": "image/jpeg",
      "size": 70922,
      "percent": 100,
      "status": "done"
    }
  ]
}

CAUTION

The following data are required for the Antd Upload component and all should be saved.

So I need to save all these fields for an Image!!

Well let’s get started with it.

Create API endpoint

Create a folder /api/media/upload

Then create a route.ts file

This endpoint needs to be a POST

import { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  try {
  } catch (error) {}
}

This is how it looks for now.

Add Uploadthing to project:

Here is my blog posts about Uploadthing

Yeah yeah not much in there!! LOL

yarn add uploadthing

Now get yourself a UPLOADTHING_TOKEN, that you can check in my blog for sure ;)

Save that token to .env file.

// Handle file upload if present
const imageFile = formData.get("image") as File;
if (imageFile) {
  const utapi = new UTApi();

  // Upload file to UploadThing
  const uploadResult = await utapi.uploadFiles([imageFile]);

  if (!uploadResult || !uploadResult[0]?.data?.url) {
    throw new Error("Image upload failed");
  }

  // Get the uploaded image URL from UploadThing
  data.imageUrl = uploadResult[0].data.url;

  if (isDevelopment) {
    console.log("Updated data with image_url:", data);
  }
}

This is the code block I used in some other project. Let’s see if this is useful.

That codeblock is converted to this:

import { NextRequest, NextResponse } from "next/server";
import { UTApi } from "uploadthing/server";

export async function POST(request: NextRequest) {
  try {
    // Handle file upload if present
    const formData = await request.formData();

    const imageFile = formData.get("image") as File;
    if (imageFile) {
      const utapi = new UTApi();

      // Upload file to UploadThing
      const uploadResult = await utapi.uploadFiles([imageFile]);

      if (!uploadResult || !uploadResult[0]?.data?.url) {
        throw new Error("Image upload failed");
      }

      return NextResponse.json({ url: uploadResult });
    }
  } catch (error) {}
}

But first let’s fix frontend issue:

Error: TypeError: url.split is not a function

This occured while adding an image to the Upload section of form.

Well I just made sure that my code was similar to the documentation one.

"use client";

import React from "react";
import { Create, useForm, getValueFromEvent } from "@refinedev/antd";
import { Form, Input, InputNumber, Upload } from "antd";
import { useApiUrl } from "@refinedev/core";

export default function DistroCreate() {
  const { formProps, saveButtonProps } = useForm();

  const apiUrl = useApiUrl();

  return (
    <Create saveButtonProps={saveButtonProps}>
      <Form {...formProps} layout="vertical">
        <Form.Item
          label="Name"
          name={["name"]}
          rules={[
            {
              required: true,
            },
          ]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="Version"
          name={["version"]}
          rules={[
            {
              required: true,
            },
          ]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="Release Year"
          name={["releaseYear"]}
          rules={[
            {
              required: true,
            },
            {
              type: "number",
            },
          ]}
        >
          <InputNumber />
        </Form.Item>
        <Form.Item
          label="Description"
          name={["description"]}
          rules={[
            {
              required: true,
            },
          ]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="Website"
          name={["website"]}
          rules={[
            {
              required: true,
            },
          ]}
        >
          <Input />
        </Form.Item>
        <Form.Item label="Logo">
          <Form.Item
            name="logo"
            getValueFromEvent={getValueFromEvent}
            noStyle
            rules={[
              {
                required: false,
              },
            ]}
          >
            <Upload.Dragger
              listType="picture"
              action={`${apiUrl}/media/upload`}
              maxCount={5}
              multiple
            >
              <p className="ant-upload-text">Drag & drop a file in this area</p>
            </Upload.Dragger>
          </Form.Item>
        </Form.Item>
      </Form>
    </Create>
  );
}

Documentation

alt text

Now the API work needs to be done.

Updated API

import { NextRequest, NextResponse } from "next/server";
import { UTApi } from "uploadthing/server";

export async function POST(request: NextRequest) {
  try {
    const formData = await request.formData();
    const file = formData.get("file") as File;

    if (!file) {
      return NextResponse.json({ error: "No file provided" }, { status: 400 });
    }

    const utapi = new UTApi();
    const uploadResult = await utapi.uploadFiles([file]);

    if (!uploadResult || !uploadResult[0]?.data?.url) {
      throw new Error("Image upload failed");
    }

    // Format response to match Antd Upload component requirements
    const response = {
      url: uploadResult[0].data.url,
      uid: `upload-${Date.now()}`, // Generate unique ID
      name: file.name,
      type: file.type,
      size: file.size,
      status: "done",
      percent: 100,
    };

    return NextResponse.json(response);
  } catch (error) {
    console.error("Upload error:", error);
    return NextResponse.json({ error: "Upload failed" }, { status: 500 });
  }
}

Now the image is being saved.

Let’s see if it saved correctly or not.

NOPE!

{
  "error": "Error creating record, PrismaClientValidationError: \nInvalid `prisma.distro.create()` invocation:\n\n{\n  data: {\n    name: \"Test\",\n    version: \"Test\",\n    releaseYear: 333,\n    description: \"Test\",\n    website: \"Test\",\n    logo: [\n      {\n        uid: \"rc-upload-1735043056500-7\",\n        lastModified: 1733672722670,\n        name: \"Admin.jpg\",\n        size: 10253,\n        type: \"image/jpeg\",\n        percent: 100,\n        originFileObj: {\n          uid: \"rc-upload-1735043056500-7\"\n        },\n        status: \"done\",\n        response: {\n          url: \"https://utfs.io/f/MwCVYPRyYlZW70kIP6JKzhMJWwHOEN298rA1LnTqjIK5xm0B\",\n          uid: \"upload-1735043349057\",\n          name: \"Admin.jpg\",\n          type: \"image/jpeg\",\n          size: 10253,\n          status: \"done\",\n          percent: 100\n        },\n        xhr: {},\n        thumbUrl: \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAFHklEQVR4Xu3dsUlsURQFUAYGLEEQM8HAwMBcsAlDU8EKzIwswgKMBRswGQRNbEK0ATMjK9ADbpHZzIofL/mc7b1nr5n5i9vzve2b59ed1cPZ9dXF6vhuefq0+/J5f3D0ePixv9w6eX+79Ny/z6bOx0JA/AHwB/D7P4AC4oR0Q/jhBiUgAiIgAmKH2NQdIt2hnSBOECeIE8QJ4gT5XUvrBHGCOEGcIE4QJ4gTBHSC3j+HblcsVyxXLFcsVyxXLFcsVyxXLFcsH6b0YdL//DCtHcQOYgexg9hB7CB2EDuIHcQOYgexg9hBfKPRNzrX5ButlnRLuiXdkm5Jt6Rb0i3plnRLuiXdkm5Jt6Rb0i3pfnfL746t/++uabG0WFosLZYWS4ulxdJiabG0WFosLZYWS4ulxdJiabG0WFos/32Clqy6JVPzGuDqAU5/e3d6X0AEREA4CAfhIByEg3AQDsJBOAgH4SAchINwEA7CQTiIlqy6JVPzGuDqAZ4cI30uIAIiIByEg3AQDsJBOAgH4SAchINwEA7CQTgIB+EgHERLVt2SqXkNcPUAp84xvS8gAiIgHISDcBAOwkE4CAfhIByEg3AQDsJBOAgH4SAcREtW3ZKpeQ1w9QBPjpE+FxABERAOwkE4CAfhIByEg3AQDsJBOAgH4SAchINwEA6iJatuydS8Brh6gFPnmN4XEAEREA7CQTgIB+EgHISDcBAOwkE4CAfhIByEg3AQDqIlq27J1LwGuHqAJ8dInwuIgAgIB+EgHISDcBAOwkE4CAfhIByEg3AQDsJBOAgH0ZJVt2RqXgNcPcCpc0zvC4iACAgH4SAchINwEA7CQTgIB+EgHISDcBAOwkE4CAfRklW3ZGpeA1w9wJNjpM8FREAEhINwEA7CQTgIB+EgHISDcBAOwkE4CAfhIByEg2jJqlsyNa8Brh7g1Dmm9wVEQASEg3AQDsJBOAgH4SAchINwEA7CQTgIB+EgHISDaMmqWzI1rwGuHuDJMdLnAiIgAsJBOAgH4SAchINwEA7CQTgIB+EgHISDcBAOwkG0ZNUtmZrXAFcPcOoc0/sCIiACwkE4CAfhIByEg3AQDsJBOAgH4SAchINwEA7CQbRk1S2ZmtcAVw/w5BjpcwEREAHhIByEg3AQDsJBOAgH4SAchINwEA7CQTgIB+EgWrLqlkzNa4CrBzh1jul9AREQAeEgHISDcBAOwkE4CAfhIByEg3AQDsJBOAgH4SBasuqWTM1rgKsHeHKM9LmACIiAcBAOwkE4CAfhIByEg3AQDsJBOAgH4SAchINwEC1ZdUum5jXA1QOcOsf0voAIiIBwEA7CQTgIB+EgHISDcBAOwkE4CAfhIByEg3AQLVl1S6bmNcDVAzw5RvpcQAREQDgIB+EgHISDcBAOwkE4CAfhIByEg3AQDsJBOIiWrLolU/Ma4OoBTp1jel9ABERAOAgH4SAchINwEA7CQTgIB+EgHISDcBAOwkE4iJasuiVT8xrg6gGeHCN9LiACIiAchINwEA7CQTgIB+EgHISDcBAOwkE4CAfhIBxES1bdkql5DXD1AKfOMb0vIAIiIByEg3AQDsJBOAgH4SAchINwEA7CQTgIB+EgHERLVt2SqXkNcPUAT46RPhcQAREQDsJBOAgH4SAchINwEA7CQTgIB+EgHISDcBAOoiWrbsnUvAa4eoBT55jeFxABERAOwkE4CAfhIByEg3AQDsJBOAgH4SAchINwEA6iJatuyb4AXOyakmpjCR4AAAAASUVORK5CYII=\"\n      }\n    ]\n    \n  }\n}\n\nArgument `logo`: Invalid value provided. Expected String or Null, provided (Object)."
}

Well now I need to change the POST to accept this type of data for record creation.

alt text

This is the root cause.

So we need to change a bit now. Not a bit but a lot.

We need to store all the meta data the AntDesign requires, for that logo should be JSON field.

Update Prisma schema

Update distros logo to be Json.

Error!!
Error validating field `logo` in model `Distro`: Field `logo` in model `Distro` can't be of type Json. The current connector does not support the Json type.Prisma

So till now we were using sqlite for database now we need to change it.

datasource db {
  provider = "postgres"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Distro {
  id          String @id @default(uuid())
  name        String
  version     String
  releaseYear Int
  description String
  website     String
  logo        Json?
}

Updated schema

Update Database Url:

Using Prisma postgres, yeah yeah I’ve blogged about it here

Get your URL for .env file and then update that.

Delete the old migration files the run :

npx prisma migrate dev --name update_logo_to_json

This is taking time, hope it works.

Great to see this: Your database is now in sync with your schema.

Great!!!

Update POST to take new logo data:

export async function POST(
  request: NextRequest,
  { params }: { params: { tableName: string } }
) {
  try {
    const { tableName } = params;
    if (!modelMap[tableName]) {
      return NextResponse.json(
        { error: "Invalid table name" },
        { status: 400 }
      );
    }

    const data = await request.json();
    const modelConfig = modelMap[tableName];

    // Deep clone the data to avoid reference issues
    let processedData = { ...data };

    // Log the complete data structure
    console.log("Full processedData:", JSON.stringify(processedData, null, 2));

    // Or if you want to see it as entries but with full detail:
    const detailedEntries = Object.entries(processedData).map(
      ([key, value]) => {
        return [key, JSON.stringify(value, null, 2)];
      }
    );
    console.log("Detailed entries:", detailedEntries);

    // Rest of your handler code...
    if (
      processedData.logo &&
      Array.isArray(processedData.logo) &&
      processedData.logo.length > 0
    ) {
      const logoData = processedData.logo[0];
      processedData.logo = {
        uid: logoData.uid,
        name: logoData.name,
        url: logoData.response?.url || logoData.url,
        type: logoData.type,
        size: logoData.size,
        status: logoData.status,
      };
    }

    // Log the processed data after transformation
    console.log("After processing:", JSON.stringify(processedData, null, 2));

    // Continue with your existing code...
    const invalidKeys = Object.keys(processedData).filter(
      (key) => !modelConfig.attributes.includes(key)
    );

    if (invalidKeys.length > 0) {
      return NextResponse.json(
        { error: `Invalid attributes: ${invalidKeys.join(", ")}` },
        { status: 400 }
      );
    }

    const result = await modelConfig.model.create({
      data: processedData,
    });

    return NextResponse.json(result, { status: 201 });
  } catch (error) {
    console.error("Error details:", error);
    if (error instanceof Error) {
      if (error.message.includes("Unique constraint")) {
        return NextResponse.json(
          { error: "Record already exists" },
          { status: 409 }
        );
      }
    }
    return NextResponse.json(
      { error: `Error creating record, ${error}` },
      { status: 500 }
    );
  }
}

alt text

Image is not being shown!!

I noticed that image from get is inside the an array

alt text

I was able to show this image but like this:

<Table.Column
  dataIndex={["logo"]}
  title="Logo"
  render={(value: any) => {
    console.log("value logo", value);
    return <ImageField style={{ maxWidth: "100px" }} value={value[0].url} />;
  }}
/>

Here this is expecting data like logo: “url to the logo”

But I think I may have got too overboard.

I guess it is time to undone what I have just done!

  1. Downdate the schema to string again.
  2. Don’t save the json data in logo field just save the url.
  3. List view to take logo as simple way.

So, the image is visible now

But the edit page is having the same issue of url.split.

The edit page also sending data like the one in create page now I need to update the /[id]/route.ts

Replicated the create API with update one.

Now the update is also working.

Sorry for the deviation above, I guess this is what happens when you wander around.

Repo link